• Unity-2D游戏-打击感与敌人AI


    前言

    最近快搞毕设了,学一些Unity2D游戏开发的知识,发现b站宝藏up主奥飒姆Awesome的两个蛮不错的教程,我想简单记录一下它这个游戏设计的方法。

    我不一点点实现了,就是分析一下大致框架(方便以后套用)


    资源

    打击感

    Red hood pixel character by Legnops

    Pixel Fantasy Caves by Szadi art.

    Pixelated Attack/Hit Animations by Viktor

    成品项目链接:

    GitHub - RedFF0000/AttackSense

    敌人AI

    Animated Pixel Adventurer by rvros

    Skeleton Sprite Pack by Jesse Munguia

    成品项目链接:

    GitHub - RedFF0000/Finite-state-machine


    动作游戏打击感

    玩家配置

    我们来看主角的动画状态:

    其中,对于标准运动定义了很多,比如idle进入run,通过分析浮点型变量Horizontal来设置,因为左右都要有动画,故设置了两个Transition:

     可以看到,除了基本的idle跑跳动作,还有一个Any State,监听了任意时刻的动作,例如我选中的any state到Light Attack1,就是当触发变量LightAttack触发时,ComboStep为1时则进入,应该就是进行轻攻击的第一下:

     这些内容的控制,主要都在代码里,我们看一下人物面板配置:

     截图中的speed调大,参考数据为 moveSpeed:180、lightSpeed:25、heavySpeed:35

    来看一下PlayerController脚本,我已经把大概的注释写好了:

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class PlayerController : MonoBehaviour
    5. {
    6. [Header("补偿速度")]
    7. public float lightSpeed;
    8. public float heavySpeed;
    9. [Header("打击感")]
    10. public float shakeTime;
    11. public int lightPause;
    12. public float lightStrength;
    13. public int heavyPause;
    14. public float heavyStrength;
    15. [Space]
    16. public float interval = 2f;
    17. private float timer;
    18. private bool isAttack;
    19. private string attackType; //攻击类型
    20. private int comboStep; //第几段攻击
    21. public float moveSpeed;
    22. public float jumpForce;
    23. new private Rigidbody2D rigidbody;
    24. private Animator animator;
    25. private float input;
    26. private bool isGround;
    27. [SerializeField] private LayerMask layer;
    28. [SerializeField] private Vector3 check;
    29. void Start()
    30. {
    31. //得到刚体
    32. rigidbody = GetComponent<Rigidbody2D>();
    33. //得到动画状态机
    34. animator = GetComponent<Animator>();
    35. }
    36. /*
    37. * 按帧执行,适合来处理GetKeyDown
    38. */
    39. void Update()
    40. {
    41. //得到水平输入
    42. input = Input.GetAxisRaw("Horizontal");
    43. //得到地面检测(layer)
    44. isGround = Physics2D.OverlapCircle(transform.position + new Vector3(check.x, check.y, 0), check.z, layer);
    45. //将刚体水平方向速度传给动画机
    46. animator.SetFloat("Horizontal", rigidbody.velocity.x);
    47. //将刚体竖直方向速度传给动画机
    48. animator.SetFloat("Vertical", rigidbody.velocity.y);
    49. //将是否触碰地面状态传给动画机
    50. animator.SetBool("isGround", isGround);
    51. //攻击处理
    52. Attack();
    53. Jump();
    54. }
    55. void Jump(){
    56. //进行跳跃
    57. if (Input.GetButtonDown("Jump") && isGround)
    58. {
    59. //给刚体一个y方向的力
    60. rigidbody.velocity = new Vector2(0, jumpForce);
    61. //同步动画机
    62. animator.SetTrigger("Jump");
    63. }
    64. }
    65. /*
    66. * 按固定时间间隔执行,适合来处理刚体效果
    67. */
    68. private void FixedUpdate()
    69. {
    70. //移动处理
    71. Move();
    72. }
    73. /*
    74. * 攻击处理函数
    75. */
    76. void Attack()
    77. {
    78. if (Input.GetKeyDown(KeyCode.J) && !isAttack) //轻击
    79. {
    80. //进入攻击状态
    81. isAttack = true;
    82. //设置攻击类型
    83. attackType = "Light";
    84. //攻击段数加一
    85. comboStep++;
    86. //还原攻击段数
    87. if (comboStep > 3)
    88. comboStep = 1;
    89. //设置攻击冷却间隔(负责自动还原攻击段数)
    90. timer = interval;
    91. //通知动画机
    92. animator.SetTrigger("LightAttack");
    93. animator.SetInteger("ComboStep", comboStep);
    94. }
    95. if (Input.GetKeyDown(KeyCode.K) && !isAttack) //重击
    96. {
    97. //重击
    98. isAttack = true;
    99. attackType = "Heavy";
    100. comboStep++;
    101. if (comboStep > 3)
    102. comboStep = 1;
    103. timer = interval;
    104. animator.SetTrigger("HeavyAttack");
    105. animator.SetInteger("ComboStep", comboStep);
    106. }
    107. if (timer != 0)
    108. {
    109. timer -= Time.deltaTime;
    110. if (timer <= 0)
    111. {
    112. timer = 0;
    113. comboStep = 0;
    114. }
    115. }
    116. }
    117. //攻击结束(此函数被每个攻击动画的快结束处调用)
    118. public void AttackOver()
    119. {
    120. isAttack = false;
    121. }
    122. /*
    123. * 移动处理函数
    124. */
    125. void Move()
    126. {
    127. if (!isAttack)
    128. //如果不在攻击中,则根据水平输入进行移动
    129. rigidbody.velocity = new Vector2(input * moveSpeed * Time.fixedDeltaTime, rigidbody.velocity.y);
    130. else
    131. {
    132. //根据攻击类型进行适量的移动(补偿速度)
    133. if (attackType == "Light")
    134. rigidbody.velocity = new Vector2(transform.localScale.x * lightSpeed * Time.fixedDeltaTime, rigidbody.velocity.y);
    135. else if (attackType == "Heavy")
    136. rigidbody.velocity = new Vector2(transform.localScale.x * heavySpeed * Time.fixedDeltaTime, rigidbody.velocity.y);
    137. }
    138. //根据速度方向调整本地缩放方向(实现转向)
    139. if (rigidbody.velocity.x < 0)
    140. transform.localScale = new Vector3(-1, 1, 1);
    141. else if (rigidbody.velocity.x > 0)
    142. transform.localScale = new Vector3(1, 1, 1);
    143. }
    144. /*
    145. * 触发检测
    146. * 玩家物体的子物体身上挂载有触发盒子
    147. * 玩家的攻击动画会动态激活子物体并调整其子物体的区域大小
    148. * 这里触发检测的就是子物体所触发区域
    149. */
    150. private void OnTriggerEnter2D(Collider2D other)
    151. {
    152. //触碰敌人
    153. if (other.CompareTag("Enemy"))
    154. {
    155. if (attackType == "Light") //轻击中
    156. {
    157. //通知攻击管理进行轻击中暂停
    158. AttackSense.Instance.HitPause(lightPause);
    159. //通知攻击管理进行轻击中镜头摇晃
    160. AttackSense.Instance.CameraShake(shakeTime, lightStrength);
    161. }
    162. else if (attackType == "Heavy") //重击中
    163. {
    164. AttackSense.Instance.HitPause(heavyPause);
    165. AttackSense.Instance.CameraShake(shakeTime, heavyStrength);
    166. }
    167. //根据自身方向调整敌人的转向
    168. if (transform.localScale.x > 0)
    169. other.GetComponent<Enemy>().GetHit(Vector2.right);
    170. else if (transform.localScale.x < 0)
    171. other.GetComponent<Enemy>().GetHit(Vector2.left);
    172. }
    173. }
    174. }

    将移动放在fixedupdate中,跳跃放在update中。
    移动和跳跃都是通过施加力实现的。跳跃有isGround变量限制,必须触地才能跳一次,故按帧执行或者按时间执行都不影响,而按时间执行还可能导致“按键失效”,所以我们放入按帧执行中,即update中;而移动是去持续施加力,在不同帧率的主机上效果不同,所以我们应该放入fixedupdate中,按时间执行

    细看代码不难理解,其中,对攻击的触发检测,触发盒挂在在子物体上,我么可以看一下大概思路:

    可以看到,打击感的核心交给了AttackSense这个单例的类,算是个攻击管理类(攻击意识);而敌人接收到攻击的响应,则是通过触发器获取敌人对象来设置,这样很好的对Enemy进行了封装,不管什么敌人,统统调用GetHit即可。

    攻击意识

    下面我们来看看攻击管理的单例类,这是一个挂载到相机上的类:

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class AttackSense : MonoBehaviour
    5. {
    6. private static AttackSense instance;
    7. public static AttackSense Instance
    8. {
    9. get
    10. {
    11. if (instance == null)
    12. instance = Transform.FindObjectOfType<AttackSense>();
    13. return instance;
    14. }
    15. }
    16. private bool isShake;
    17. public void HitPause(int duration)
    18. {
    19. StartCoroutine(Pause(duration));
    20. }
    21. IEnumerator Pause(int duration)
    22. {
    23. float pauseTime = duration / 60f;
    24. Time.timeScale = 0;
    25. yield return new WaitForSecondsRealtime(pauseTime);
    26. Time.timeScale = 1;
    27. }
    28. public void CameraShake(float duration, float strength)
    29. {
    30. if (!isShake)
    31. StartCoroutine(Shake(duration, strength));
    32. }
    33. IEnumerator Shake(float duration, float strength)
    34. {
    35. isShake = true;
    36. Transform camera = Camera.main.transform;
    37. Vector3 startPosition = camera.position;
    38. while (duration > 0)
    39. {
    40. camera.position = Random.insideUnitSphere * strength + startPosition;
    41. duration -= Time.deltaTime;
    42. yield return null;
    43. }
    44. camera.position = startPosition;
    45. isShake = false;
    46. }
    47. }

    很简单

    敌人配置

    最后我们来看一下Enemy的配置,这里敌人的动画很简单:

     

    代码如下:

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class Enemy : MonoBehaviour
    5. {
    6. public float speed;
    7. private Vector2 direction;
    8. private bool isHit;
    9. new private Rigidbody2D rigidbody;
    10. private AnimatorStateInfo info;
    11. private Animator animator;
    12. void Start()
    13. {
    14. //得到自身动画机
    15. animator = transform.GetComponent<Animator>();
    16. //得到刚体
    17. rigidbody = transform.GetComponent<Rigidbody2D>();
    18. }
    19. void Update()
    20. {
    21. //得到自身动画机动画状态信息
    22. info = animator.GetCurrentAnimatorStateInfo(0);
    23. if (isHit)
    24. {
    25. //受到攻击回退
    26. rigidbody.velocity = direction * speed;
    27. if (info.normalizedTime >= .6f)
    28. isHit = false;
    29. }
    30. }
    31. public void GetHit(Vector2 direction)
    32. {
    33. //与受攻击方向同步方向
    34. transform.localScale = new Vector3(-direction.x, 1, 1);
    35. isHit = true;
    36. //重置方向,便于回退Update中
    37. this.direction = direction;
    38. //同步动画状态
    39. animator.SetTrigger("Hit");
    40. }
    41. }

    结语

    基本配置完成,打击感满满

     

    敌方AI

    玩家配置

    玩家的配置很简单,就是正常的移动跳跃,需要更高级的配置在后面可以自己添加:

     

    敌人配置

    我们重点来看敌人配置:

    在Enemy下有两个子物体:

    • Area:一个长条的空物体。触发器,模拟敌人的检测视野
    • AttackArea:一个空物体(一个点)。将在脚本中搭配一个半径值来构成一个圆形区域,作为敌人施展攻击的触发区域

    在Enemy上,绑定着脚本FSM(有限状态机),其内容我已注释:

    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. //定义枚举类型
    6. public enum StateType
    7. {
    8. //待机、巡逻、追逐、反应、攻击、被攻击、死亡
    9. Idle, Patrol, Chase, React, Attack, Hit, Death
    10. }
    11. [Serializable] //可序列化-在检视面板可以设置
    12. public class Parameter
    13. {
    14. //生命值
    15. public int health;
    16. //巡逻速度
    17. public float moveSpeed;
    18. //追逐速度
    19. public float chaseSpeed;
    20. //待机时长(巡逻到头后待机)
    21. public float idleTime;
    22. //巡逻范围
    23. public Transform[] patrolPoints;
    24. //追逐范围
    25. public Transform[] chasePoints;
    26. //目标
    27. public Transform target;
    28. //目标所在层
    29. public LayerMask targetLayer;
    30. //攻击位置点
    31. public Transform attackPoint;
    32. //攻击位置点的半径(结合攻击位置点规划出一个攻击触发区)
    33. public float attackArea;
    34. //动画机
    35. public Animator animator;
    36. //是否受攻击
    37. public bool getHit;
    38. }
    39. //定义有限状态机类
    40. public class FSM : MonoBehaviour
    41. {
    42. //当前状态
    43. private IState currentState;
    44. //状态字典
    45. private Dictionary<StateType, IState> states = new Dictionary<StateType, IState>();
    46. //实例化一个parameter对象
    47. public Parameter parameter;
    48. void Start()
    49. {
    50. //给字典添加各个枚举类型对应的各个对象
    51. states.Add(StateType.Idle, new IdleState(this));
    52. states.Add(StateType.Patrol, new PatrolState(this));
    53. states.Add(StateType.Chase, new ChaseState(this));
    54. states.Add(StateType.React, new ReactState(this));
    55. states.Add(StateType.Attack, new AttackState(this));
    56. states.Add(StateType.Hit, new HitState(this));
    57. states.Add(StateType.Death, new DeathState(this));
    58. //初始化状态为Idle状态(转换为Idle状态)
    59. TransitionState(StateType.Idle);
    60. //初始化参数中的动画机
    61. parameter.animator = transform.GetComponent<Animator>();
    62. }
    63. void Update()
    64. {
    65. //当前状态更新
    66. currentState.OnUpdate();
    67. //模拟受到攻击(按下回车)
    68. if (Input.GetKeyDown(KeyCode.Return))
    69. {
    70. //参数对象设置受到攻击
    71. parameter.getHit = true;
    72. }
    73. }
    74. /* 状态转换
    75. *
    76. */
    77. public void TransitionState(StateType type)
    78. {
    79. //当前已有状态,做退出工作
    80. if (currentState != null)
    81. currentState.OnExit();
    82. //更改当前状态
    83. currentState = states[type];
    84. //做进入状态工作
    85. currentState.OnEnter();
    86. }
    87. /* 翻转
    88. * 根据目标位置转向
    89. */
    90. public void FlipTo(Transform target)
    91. {
    92. if (target != null)
    93. {
    94. if (transform.position.x > target.position.x)
    95. {
    96. transform.localScale = new Vector3(-1, 1, 1);
    97. }
    98. else if (transform.position.x < target.position.x)
    99. {
    100. transform.localScale = new Vector3(1, 1, 1);
    101. }
    102. }
    103. }
    104. //碰到玩家触发(触发器是子物体Area
    105. private void OnTriggerEnter2D(Collider2D other)
    106. {
    107. if (other.CompareTag("Player"))
    108. {
    109. parameter.target = other.transform;
    110. }
    111. }
    112. //退出触发
    113. private void OnTriggerExit2D(Collider2D other)
    114. {
    115. if (other.CompareTag("Player"))
    116. {
    117. parameter.target = null;
    118. }
    119. }
    120. private void OnDrawGizmos()
    121. {
    122. Gizmos.DrawWireSphere(parameter.attackPoint.position, parameter.attackArea);
    123. }
    124. }

    可以看到,原作者是通过状态机来实现AI的,将各个状态封装成对象,全部继承自接口:

    1. public interface IState
    2. {
    3. void OnEnter();
    4. void OnUpdate();
    5. void OnExit();
    6. }

    下面我们来一个个看,代码上我就省略头文件的引用了:

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;

    待机状态:

    1. /*
    2. * 待机状态
    3. */
    4. public class IdleState : IState
    5. {
    6. //一个有限状态机
    7. private FSM manager;
    8. //一个参数汇总对象
    9. private Parameter parameter;
    10. private float timer;
    11. //初始化
    12. public IdleState(FSM manager)
    13. {
    14. this.manager = manager;
    15. this.parameter = manager.parameter;
    16. }
    17. public void OnEnter()
    18. {
    19. //参数对象的动画机播放待机动画(待机动画是循环的)
    20. parameter.animator.Play("Idle");
    21. }
    22. public void OnUpdate()
    23. {
    24. //计时器累积(单位秒)
    25. timer += Time.deltaTime;
    26. //待机状态被打
    27. if (parameter.getHit)
    28. {
    29. //转换状态到Hit
    30. manager.TransitionState(StateType.Hit);
    31. }
    32. //发现目标且目标距离在追捕范围内
    33. if (parameter.target != null &&
    34. parameter.target.position.x >= parameter.chasePoints[0].position.x &&
    35. parameter.target.position.x <= parameter.chasePoints[1].position.x)
    36. {
    37. //进入反应状态
    38. manager.TransitionState(StateType.React);
    39. }
    40. //计时器超过待机时间
    41. if (timer >= parameter.idleTime)
    42. {
    43. //转换回巡逻状态
    44. manager.TransitionState(StateType.Patrol);
    45. }
    46. }
    47. public void OnExit()
    48. {
    49. //计时器归零
    50. timer = 0;
    51. }
    52. }

    巡逻状态:

    1. /*
    2. * 巡逻状态
    3. */
    4. public class PatrolState : IState
    5. {
    6. private FSM manager;
    7. private Parameter parameter;
    8. private int patrolPosition;
    9. //初始化
    10. public PatrolState(FSM manager)
    11. {
    12. this.manager = manager;
    13. this.parameter = manager.parameter;
    14. }
    15. public void OnEnter()
    16. {
    17. //播放走路动画
    18. parameter.animator.Play("Walk");
    19. }
    20. public void OnUpdate()
    21. {
    22. //根据巡逻位置转向
    23. manager.FlipTo(parameter.patrolPoints[patrolPosition]);
    24. //位移
    25. manager.transform.position = Vector2.MoveTowards(manager.transform.position,
    26. parameter.patrolPoints[patrolPosition].position, parameter.moveSpeed * Time.deltaTime);
    27. if (parameter.getHit)
    28. {
    29. manager.TransitionState(StateType.Hit);
    30. }
    31. if (parameter.target != null &&
    32. parameter.target.position.x >= parameter.chasePoints[0].position.x &&
    33. parameter.target.position.x <= parameter.chasePoints[1].position.x)
    34. {
    35. manager.TransitionState(StateType.React);
    36. }
    37. if (Vector2.Distance(manager.transform.position, parameter.patrolPoints[patrolPosition].position) < .1f)
    38. {
    39. manager.TransitionState(StateType.Idle);
    40. }
    41. }
    42. public void OnExit()
    43. {
    44. //巡逻点下标更新
    45. patrolPosition++;
    46. if (patrolPosition >= parameter.patrolPoints.Length)
    47. {
    48. patrolPosition = 0;
    49. }
    50. }
    51. }

    追捕状态:

    1. /*
    2. * 追捕状态
    3. */
    4. public class ChaseState : IState
    5. {
    6. private FSM manager;
    7. private Parameter parameter;
    8. public ChaseState(FSM manager)
    9. {
    10. this.manager = manager;
    11. this.parameter = manager.parameter;
    12. }
    13. public void OnEnter()
    14. {
    15. //播放走路动画(Walk动画循环)
    16. parameter.animator.Play("Walk");
    17. }
    18. public void OnUpdate()
    19. {
    20. //根据目标位置转向
    21. manager.FlipTo(parameter.target);
    22. //有目标则一直走
    23. if (parameter.target)
    24. manager.transform.position = Vector2.MoveTowards(manager.transform.position,
    25. parameter.target.position, parameter.chaseSpeed * Time.deltaTime);
    26. if (parameter.getHit)
    27. {
    28. manager.TransitionState(StateType.Hit);
    29. }
    30. if (parameter.target == null ||
    31. manager.transform.position.x < parameter.chasePoints[0].position.x ||
    32. manager.transform.position.x > parameter.chasePoints[1].position.x)
    33. {
    34. manager.TransitionState(StateType.Idle);
    35. }
    36. if (Physics2D.OverlapCircle(parameter.attackPoint.position, parameter.attackArea, parameter.targetLayer))
    37. {
    38. manager.TransitionState(StateType.Attack);
    39. }
    40. }
    41. public void OnExit()
    42. {
    43. }
    44. }

    反应状态:

    1. /*
    2. * 反应状态
    3. */
    4. public class ReactState : IState
    5. {
    6. private FSM manager;
    7. private Parameter parameter;
    8. //得到动画状态
    9. private AnimatorStateInfo info;
    10. public ReactState(FSM manager)
    11. {
    12. this.manager = manager;
    13. this.parameter = manager.parameter;
    14. }
    15. public void OnEnter()
    16. {
    17. parameter.animator.Play("React");
    18. }
    19. public void OnUpdate()
    20. {
    21. //得到当前动画状态
    22. info = parameter.animator.GetCurrentAnimatorStateInfo(0);
    23. if (parameter.getHit)
    24. {
    25. manager.TransitionState(StateType.Hit);
    26. }
    27. //播放动画快结束,则进入追捕状态
    28. if (info.normalizedTime >= .95f)
    29. {
    30. manager.TransitionState(StateType.Chase);
    31. }
    32. }
    33. public void OnExit()
    34. {
    35. }
    36. }

    攻击状态

    1. /*
    2. * 攻击状态
    3. */
    4. public class AttackState : IState
    5. {
    6. private FSM manager;
    7. private Parameter parameter;
    8. private AnimatorStateInfo info;
    9. public AttackState(FSM manager)
    10. {
    11. this.manager = manager;
    12. this.parameter = manager.parameter;
    13. }
    14. public void OnEnter()
    15. {
    16. //进入攻击动画
    17. parameter.animator.Play("Attack");
    18. }
    19. public void OnUpdate()
    20. {
    21. info = parameter.animator.GetCurrentAnimatorStateInfo(0);
    22. if (parameter.getHit)
    23. {
    24. manager.TransitionState(StateType.Hit);
    25. }
    26. if (info.normalizedTime >= .95f)
    27. {
    28. manager.TransitionState(StateType.Chase);
    29. }
    30. }
    31. public void OnExit()
    32. {
    33. }
    34. }

    受攻击状态:

    1. /*
    2. * 受攻击状态
    3. */
    4. public class HitState : IState
    5. {
    6. private FSM manager;
    7. private Parameter parameter;
    8. private AnimatorStateInfo info;
    9. public HitState(FSM manager)
    10. {
    11. this.manager = manager;
    12. this.parameter = manager.parameter;
    13. }
    14. public void OnEnter()
    15. {
    16. //播放动画,
    17. parameter.animator.Play("Hit");
    18. //掉血
    19. parameter.health--;
    20. }
    21. public void OnUpdate()
    22. {
    23. info = parameter.animator.GetCurrentAnimatorStateInfo(0);
    24. if (parameter.health <= 0)
    25. {
    26. //死亡
    27. manager.TransitionState(StateType.Death);
    28. }
    29. if (info.normalizedTime >= .95f)
    30. {
    31. parameter.target = GameObject.FindWithTag("Player").transform;
    32. manager.TransitionState(StateType.Chase);
    33. }
    34. }
    35. public void OnExit()
    36. {
    37. parameter.getHit = false;
    38. }
    39. }

    死亡状态:

    1. /*
    2. * 死亡状态
    3. */
    4. public class DeathState : IState
    5. {
    6. private FSM manager;
    7. private Parameter parameter;
    8. public DeathState(FSM manager)
    9. {
    10. this.manager = manager;
    11. this.parameter = manager.parameter;
    12. }
    13. public void OnEnter()
    14. {
    15. parameter.animator.Play("Dead");
    16. }
    17. public void OnExit()
    18. {
    19. throw new System.NotImplementedException();
    20. }
    21. public void OnUpdate()
    22. {
    23. throw new System.NotImplementedException();
    24. }
    25. }

    结语

    功能基本完成。

  • 相关阅读:
    利用Python爬虫 爬取金融期货数据
    释放WebKit潜能:硬件加速的秘诀与实战
    C语言:static,volatile,const,extern,register,auto, 栈stack结构
    一篇让你使用vue-cli搭建SPA项目
    【机器学习5】无监督学习聚类
    Java 加载、编辑和保存WPS表格文件(.et/.ett)
    C语言-数组指针与指针数组
    《吐血整理》高级系列教程-吃透Fiddler抓包教程(29)-Fiddler如何抓取Android7.0以上的Https包-终篇
    vue3 父子组件之间的方法调用
    C++入门
  • 原文地址:https://blog.csdn.net/m0_46378049/article/details/128078913