Unity-2D游戏-打击感与敌人AI
创始人
2024-02-25 16:02:09
0

前言

最近快搞毕设了,学一些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脚本,我已经把大概的注释写好了:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerController : MonoBehaviour
{[Header("补偿速度")]public float lightSpeed;public float heavySpeed;[Header("打击感")]public float shakeTime;public int lightPause;public float lightStrength;public int heavyPause;public float heavyStrength;[Space]public float interval = 2f;private float timer;private bool isAttack;private string attackType; //攻击类型private int comboStep; //第几段攻击public float moveSpeed;public float jumpForce;new private Rigidbody2D rigidbody;private Animator animator;private float input;private bool isGround;[SerializeField] private LayerMask layer;[SerializeField] private Vector3 check;void Start(){//得到刚体rigidbody = GetComponent();//得到动画状态机animator = GetComponent();}/** 按帧执行,适合来处理GetKeyDown*/void Update(){//得到水平输入input = Input.GetAxisRaw("Horizontal");//得到地面检测(layer)isGround = Physics2D.OverlapCircle(transform.position + new Vector3(check.x, check.y, 0), check.z, layer);//将刚体水平方向速度传给动画机animator.SetFloat("Horizontal", rigidbody.velocity.x);//将刚体竖直方向速度传给动画机animator.SetFloat("Vertical", rigidbody.velocity.y);//将是否触碰地面状态传给动画机animator.SetBool("isGround", isGround);//攻击处理Attack();Jump();}void Jump(){//进行跳跃if (Input.GetButtonDown("Jump") && isGround){//给刚体一个y方向的力rigidbody.velocity = new Vector2(0, jumpForce);//同步动画机animator.SetTrigger("Jump");}}/** 按固定时间间隔执行,适合来处理刚体效果*/private void FixedUpdate(){//移动处理Move();}/** 攻击处理函数*/void Attack(){if (Input.GetKeyDown(KeyCode.J) && !isAttack) //轻击{//进入攻击状态isAttack = true;//设置攻击类型attackType = "Light";//攻击段数加一comboStep++;//还原攻击段数if (comboStep > 3)comboStep = 1;//设置攻击冷却间隔(负责自动还原攻击段数)timer = interval;//通知动画机animator.SetTrigger("LightAttack");animator.SetInteger("ComboStep", comboStep);}if (Input.GetKeyDown(KeyCode.K) && !isAttack) //重击{//重击isAttack = true;attackType = "Heavy";comboStep++;if (comboStep > 3)comboStep = 1;timer = interval;animator.SetTrigger("HeavyAttack");animator.SetInteger("ComboStep", comboStep);}if (timer != 0){timer -= Time.deltaTime;if (timer <= 0){timer = 0;comboStep = 0;}}}//攻击结束(此函数被每个攻击动画的快结束处调用)public void AttackOver(){isAttack = false;}/** 移动处理函数*/void Move(){if (!isAttack)//如果不在攻击中,则根据水平输入进行移动rigidbody.velocity = new Vector2(input * moveSpeed * Time.fixedDeltaTime, rigidbody.velocity.y);else{//根据攻击类型进行适量的移动(补偿速度)if (attackType == "Light")rigidbody.velocity = new Vector2(transform.localScale.x * lightSpeed * Time.fixedDeltaTime, rigidbody.velocity.y);else if (attackType == "Heavy")rigidbody.velocity = new Vector2(transform.localScale.x * heavySpeed * Time.fixedDeltaTime, rigidbody.velocity.y);}//根据速度方向调整本地缩放方向(实现转向)if (rigidbody.velocity.x < 0)transform.localScale = new Vector3(-1, 1, 1);else if (rigidbody.velocity.x > 0)transform.localScale = new Vector3(1, 1, 1);}/** 触发检测* 玩家物体的子物体身上挂载有触发盒子* 玩家的攻击动画会动态激活子物体并调整其子物体的区域大小* 这里触发检测的就是子物体所触发区域*/private void OnTriggerEnter2D(Collider2D other){//触碰敌人if (other.CompareTag("Enemy")){if (attackType == "Light") //轻击中{//通知攻击管理进行轻击中暂停AttackSense.Instance.HitPause(lightPause);//通知攻击管理进行轻击中镜头摇晃AttackSense.Instance.CameraShake(shakeTime, lightStrength); }else if (attackType == "Heavy") //重击中{AttackSense.Instance.HitPause(heavyPause);AttackSense.Instance.CameraShake(shakeTime, heavyStrength);}//根据自身方向调整敌人的转向if (transform.localScale.x > 0)other.GetComponent().GetHit(Vector2.right);else if (transform.localScale.x < 0)other.GetComponent().GetHit(Vector2.left);}}
}

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

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

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

攻击意识

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class AttackSense : MonoBehaviour
{private static AttackSense instance;public static AttackSense Instance{get{if (instance == null)instance = Transform.FindObjectOfType();return instance;}}private bool isShake;public void HitPause(int duration){StartCoroutine(Pause(duration));}IEnumerator Pause(int duration){float pauseTime = duration / 60f;Time.timeScale = 0;yield return new WaitForSecondsRealtime(pauseTime);Time.timeScale = 1;}public void CameraShake(float duration, float strength){if (!isShake)StartCoroutine(Shake(duration, strength));}IEnumerator Shake(float duration, float strength){isShake = true;Transform camera = Camera.main.transform;Vector3 startPosition = camera.position;while (duration > 0){camera.position = Random.insideUnitSphere * strength + startPosition;duration -= Time.deltaTime;yield return null;}camera.position = startPosition;isShake = false;}
}

很简单

敌人配置

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

 

代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Enemy : MonoBehaviour
{public float speed;private Vector2 direction;private bool isHit;new private Rigidbody2D rigidbody;private AnimatorStateInfo info;private Animator animator;void Start(){//得到自身动画机animator = transform.GetComponent();//得到刚体rigidbody = transform.GetComponent();}void Update(){//得到自身动画机动画状态信息info = animator.GetCurrentAnimatorStateInfo(0);if (isHit){//受到攻击回退rigidbody.velocity = direction * speed; if (info.normalizedTime >= .6f)isHit = false;}}public void GetHit(Vector2 direction){//与受攻击方向同步方向transform.localScale = new Vector3(-direction.x, 1, 1); isHit = true;//重置方向,便于回退Update中this.direction = direction;//同步动画状态animator.SetTrigger("Hit");}
}

结语

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

 

敌方AI

玩家配置

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

 

敌人配置

我们重点来看敌人配置:

在Enemy下有两个子物体:

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

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

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;//定义枚举类型
public enum StateType
{//待机、巡逻、追逐、反应、攻击、被攻击、死亡Idle, Patrol, Chase, React, Attack, Hit, Death
}[Serializable] //可序列化-在检视面板可以设置
public class Parameter
{//生命值public int health;//巡逻速度public float moveSpeed;//追逐速度public float chaseSpeed;//待机时长(巡逻到头后待机)public float idleTime;//巡逻范围public Transform[] patrolPoints;//追逐范围public Transform[] chasePoints;//目标public Transform target;//目标所在层public LayerMask targetLayer;//攻击位置点public Transform attackPoint;//攻击位置点的半径(结合攻击位置点规划出一个攻击触发区)public float attackArea;//动画机public Animator animator;//是否受攻击public bool getHit;
}
//定义有限状态机类
public class FSM : MonoBehaviour
{//当前状态private IState currentState;//状态字典private Dictionary states = new Dictionary();//实例化一个parameter对象public Parameter parameter;void Start(){//给字典添加各个枚举类型对应的各个对象states.Add(StateType.Idle, new IdleState(this));states.Add(StateType.Patrol, new PatrolState(this));states.Add(StateType.Chase, new ChaseState(this));states.Add(StateType.React, new ReactState(this));states.Add(StateType.Attack, new AttackState(this));states.Add(StateType.Hit, new HitState(this));states.Add(StateType.Death, new DeathState(this));//初始化状态为Idle状态(转换为Idle状态)TransitionState(StateType.Idle);//初始化参数中的动画机parameter.animator = transform.GetComponent();}void Update(){//当前状态更新currentState.OnUpdate();//模拟受到攻击(按下回车)if (Input.GetKeyDown(KeyCode.Return)){//参数对象设置受到攻击parameter.getHit = true;}}/* 状态转换* */public void TransitionState(StateType type){//当前已有状态,做退出工作if (currentState != null)currentState.OnExit();//更改当前状态currentState = states[type];//做进入状态工作currentState.OnEnter();}/* 翻转* 根据目标位置转向*/public void FlipTo(Transform target){if (target != null){if (transform.position.x > target.position.x){transform.localScale = new Vector3(-1, 1, 1);}else if (transform.position.x < target.position.x){transform.localScale = new Vector3(1, 1, 1);}}}//碰到玩家触发(触发器是子物体Area)private void OnTriggerEnter2D(Collider2D other){if (other.CompareTag("Player")){parameter.target = other.transform;}}//退出触发private void OnTriggerExit2D(Collider2D other){if (other.CompareTag("Player")){parameter.target = null;}}private void OnDrawGizmos(){Gizmos.DrawWireSphere(parameter.attackPoint.position, parameter.attackArea);}
}

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

public interface IState
{void OnEnter();void OnUpdate();void OnExit();
}

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

待机状态:

/** 待机状态*/
public class IdleState : IState
{//一个有限状态机private FSM manager;//一个参数汇总对象private Parameter parameter;private float timer;//初始化public IdleState(FSM manager){this.manager = manager;this.parameter = manager.parameter;}public void OnEnter(){//参数对象的动画机播放待机动画(待机动画是循环的)parameter.animator.Play("Idle");}public void OnUpdate(){//计时器累积(单位秒)timer += Time.deltaTime;//待机状态被打if (parameter.getHit){//转换状态到Hitmanager.TransitionState(StateType.Hit);}//发现目标且目标距离在追捕范围内if (parameter.target != null &¶meter.target.position.x >= parameter.chasePoints[0].position.x &¶meter.target.position.x <= parameter.chasePoints[1].position.x){//进入反应状态manager.TransitionState(StateType.React);}//计时器超过待机时间if (timer >= parameter.idleTime){//转换回巡逻状态manager.TransitionState(StateType.Patrol);}}public void OnExit(){//计时器归零timer = 0;}
}

巡逻状态:

/** 巡逻状态*/
public class PatrolState : IState
{private FSM manager;private Parameter parameter;private int patrolPosition;//初始化public PatrolState(FSM manager){this.manager = manager;this.parameter = manager.parameter;}public void OnEnter(){//播放走路动画parameter.animator.Play("Walk");}public void OnUpdate(){//根据巡逻位置转向manager.FlipTo(parameter.patrolPoints[patrolPosition]);//位移manager.transform.position = Vector2.MoveTowards(manager.transform.position,parameter.patrolPoints[patrolPosition].position, parameter.moveSpeed * Time.deltaTime);if (parameter.getHit){manager.TransitionState(StateType.Hit);}if (parameter.target != null &¶meter.target.position.x >= parameter.chasePoints[0].position.x &¶meter.target.position.x <= parameter.chasePoints[1].position.x){manager.TransitionState(StateType.React);}if (Vector2.Distance(manager.transform.position, parameter.patrolPoints[patrolPosition].position) < .1f){manager.TransitionState(StateType.Idle);}}public void OnExit(){//巡逻点下标更新patrolPosition++;if (patrolPosition >= parameter.patrolPoints.Length){patrolPosition = 0;}}
}

追捕状态:

/** 追捕状态*/
public class ChaseState : IState
{private FSM manager;private Parameter parameter;public ChaseState(FSM manager){this.manager = manager;this.parameter = manager.parameter;}public void OnEnter(){//播放走路动画(Walk动画循环)parameter.animator.Play("Walk");}public void OnUpdate(){//根据目标位置转向manager.FlipTo(parameter.target);//有目标则一直走if (parameter.target)manager.transform.position = Vector2.MoveTowards(manager.transform.position,parameter.target.position, parameter.chaseSpeed * Time.deltaTime);if (parameter.getHit){manager.TransitionState(StateType.Hit);}if (parameter.target == null ||manager.transform.position.x < parameter.chasePoints[0].position.x ||manager.transform.position.x > parameter.chasePoints[1].position.x){manager.TransitionState(StateType.Idle);}if (Physics2D.OverlapCircle(parameter.attackPoint.position, parameter.attackArea, parameter.targetLayer)){manager.TransitionState(StateType.Attack);}}public void OnExit(){}
}

反应状态:

/** 反应状态*/
public class ReactState : IState
{private FSM manager;private Parameter parameter;//得到动画状态private AnimatorStateInfo info;public ReactState(FSM manager){this.manager = manager;this.parameter = manager.parameter;}public void OnEnter(){parameter.animator.Play("React");}public void OnUpdate(){//得到当前动画状态info = parameter.animator.GetCurrentAnimatorStateInfo(0);if (parameter.getHit){ manager.TransitionState(StateType.Hit);}//播放动画快结束,则进入追捕状态if (info.normalizedTime >= .95f){manager.TransitionState(StateType.Chase);}}public void OnExit(){}
}

攻击状态

/** 攻击状态*/
public class AttackState : IState
{private FSM manager;private Parameter parameter;private AnimatorStateInfo info;public AttackState(FSM manager){this.manager = manager;this.parameter = manager.parameter;}public void OnEnter(){//进入攻击动画parameter.animator.Play("Attack");}public void OnUpdate(){info = parameter.animator.GetCurrentAnimatorStateInfo(0);if (parameter.getHit){manager.TransitionState(StateType.Hit);}if (info.normalizedTime >= .95f){manager.TransitionState(StateType.Chase);}}public void OnExit(){}
}

受攻击状态:

/** 受攻击状态*/
public class HitState : IState
{private FSM manager;private Parameter parameter;private AnimatorStateInfo info;public HitState(FSM manager){this.manager = manager;this.parameter = manager.parameter;}public void OnEnter(){//播放动画,parameter.animator.Play("Hit");//掉血parameter.health--;}public void OnUpdate(){info = parameter.animator.GetCurrentAnimatorStateInfo(0);if (parameter.health <= 0){//死亡manager.TransitionState(StateType.Death);}if (info.normalizedTime >= .95f){parameter.target = GameObject.FindWithTag("Player").transform;manager.TransitionState(StateType.Chase);}}public void OnExit(){parameter.getHit = false;}
}

死亡状态:

/** 死亡状态*/
public class DeathState : IState
{private FSM manager;private Parameter parameter;public DeathState(FSM manager){this.manager = manager;this.parameter = manager.parameter;}public void OnEnter(){parameter.animator.Play("Dead");}public void OnExit(){throw new System.NotImplementedException();}public void OnUpdate(){throw new System.NotImplementedException();}
}

结语

功能基本完成。

相关内容

热门资讯

青春华章•青春问答 | 当AI...   当AI以前所未有的速度发展,您是否也在担心被取代、被超越?如何在智能浪潮中稳住心态、找准定位、实...
中国人民银行发布一次性信用修复...   为支持信用受损但积极还款的个人高效便捷重塑信用,中国人民银行12月22日对外发布一次性信用修复政...
【学习贯彻党的二十届四中全会精...   连日来,河北、江苏、广西、北京大学结合自身实际、深入基层一线,开展多种形式的宣讲活动,让党的二十...
日本多名教职人员因性暴力或性犯...   据日本文部科学省22日公布的数据,自2024年4月至2025年3月,日本共有281名公立学校教职...
百千万·山海间|五口人,50年   你知道吗?  全国每5个柚子,就有1个来自广东梅州。  梅州梅县区雁洋镇的南福村是一个种柚大村。...
转向军国旧路,必将自取灭亡丨新...   日本首相高市早苗近期在国会发表的涉台谬论,绝非简单的“口误”或“失态”,而是一场以国家前途和地区...
持续推进便民建设 各地居民生活...   央视网消息(新闻联播):各地持续推进便民服务设施建设,出台各类民生改善举措,让居民生活更温馨、更...
视频丨跨省奔赴音乐之约 演唱会...   近年来,大型演出市场呈持续上升态势,演唱会、音乐节等演出不但聚集人气带来票房收入,还带动交通、住...
祖国大家庭的温暖丨烘焙坊里爱的...   12月17日,乌鲁木齐新禾特青烘焙坊的烘焙间,水声沥沥。24岁的小赵正熟练地将使用过的工具一一清...
市场监管总局对充电宝等高风险产...   央视网消息:据市场监管总局消息,为严格落实获证生产企业质量安全主体责任,充分发挥CCC认证管理制...