赞
踩
素材1链接
素材2链接
将需要的素材文件拖拽到Project下的Assets目录下。点击素材Inspector窗口会显示素材信息。注意素材的尺寸,采样模式等,修改成需要尺寸。如果素材是图集,Sprite Mode需要设置成Multiple,需要点击Sprite Editor对图集进行切割。
进入Sprite Editor界面,点击Slice设置切割方式,注意锚点的位置。选择合适的切割方式,点击Slice切割,点击apply保存。
切割好之后就有一系列图片。
点击Window菜单,找到2D目录下的Tile Palette。
在Assets路径下创建调色盘。将切割好的图片素材拖入调色盘文件夹中,会出现弹框要求创建一个tile文件夹存放瓦片素材。在调色盘同级目录下创建该文件夹。
在Hierarchy窗口中,选择2D Object创建Tilemap。在Tile Palette窗口中就会出现对应的TileMap。谈后就可以选择对应的瓦片绘制场景地图了。
一个场景可以绘制多个地图用来展示不同的图层。点击地图,在Inspector窗口中通过设置Sorting Layer和Order in Layer来控制图层的显示前后顺序,前者优先级更高。
如果觉得徒手绘制场景地图太麻烦,unity还提供了自动生成场景地图的功能。通过编辑瓦片的位置条件,可以让该瓦片出现在你想要的位置上。
在资源文件瓦片文件夹下创建一个RuleTiles文件夹用来存放该系列瓦片。
在该文件下选择创建Relu Tile。
得到瓦片文件
通过编辑该瓦片出现的条件使瓦片出现在你想要的位置,条件的优先级越往下越低。
设置好后将该瓦片文件拖入着色盘中,选择该瓦片,选择矩形绘制工具进行绘制,绘制的地形就会通过你的设置进行填充了。
unity还可以制作动态的地形。操作和ReluTiles类似,需要创建AnimatedTiles。
同样将文件拖入着色盘后进行绘制。播放游戏可以看到动态效果。如果觉得动态瓦片之间有缝隙,可以调节Grid的Cell Gap为-0.01就可以解决。
要实现角色跳跃和降落等和现实世界相符合的物理行为需要给对象添加物理特性,unity中有专门的实现方法。
Rigidbody2D组件:为对象添加该组件可以让该对象具有一些物理属性
因为角色具有重力属性,有时角色会因为重心而翻转,我们需要锁定角色的旋转属性。
要实现角色站立在地面上,需要给角色和地面都添加碰撞体组件。
为角色添加胶囊形状的碰撞体
为地面添加地形专用的碰撞体。
我们希望地面是静止不动的,需要把BodyType设置成Static.
添加完碰撞体之后角色就可以站立在地面上了。
为了使游戏可以在不同的操作系统和操作设备时都能控制游戏,我们可以使用unity最新的输入系统。
点击Editor->Project Settings
点击Player,往下滑找到Configuration,将Api Compatibility Level修改成 .NET Framework.
Active Input Handling修改成Input System Package(New),保存并重启unity编辑器。
点击Windows->Package Manager
打开包管理器之后等内容加载完,选择Unity Registry。在搜索框搜索input找到下面的包,点击安装。
在Assets->Settings目录下创建新的输入系统。右键点击Create,往下滑找到 Input Actions
点击该输入系统,点击Edit asset进行编辑
在编辑界面,点击+添加新的Action Maps,点击Actions添加Action。Action Type可以选择以按钮或以值来检测。
点击Movement右侧的+可以快捷的创建上下左右绑定。Path可以设置对应的按钮。(按W向上)
通过该输入系统可以设置多种输入方式。
unity可以自动生成输入配置表。为player添加一个Player Input的组件 ,并创建默认输入配置表。可以通过使用Player Input来控制角色的行为。
也可以不使用Player Input而选择通过脚本控制角色行为
选中生成的输入系统,点击Apply,生成和生成的输入系统同名的脚本。
为Player创建一个用于控制角色行为的脚本。
Awake会最先调用,然后在调用OnEnable,Update每一帧都会调用,FixedUpdate每隔一定时间就会被调用一次。因为电脑的帧率会变,运动的函数放在FixedUpdate中不会让角色移动速度忽快忽慢。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem;//引用InputSystem库 public class PlayerController : MonoBehaviour { public PlayerInputControl inputControl;//声明PlayerInputControl变量,PlayerInputControl是之前输入生成的脚本 public Vector2 inputDirection;//设置方向 public Rigidbody2D rb;//声明Rigidbody2D变量 public float speed; private void Awake() {//这个函数在开始时会调用 inputControl = new PlayerInputControl();//实例化PlayerInputControl } private void OnEnable() { inputControl.Enable();//启动输入对象 } private void OnDisable() { inputControl.Disable();//关闭输入对象 } private void Update() {//这个函数每一帧都会调用 inputDirection = inputControl.Gameplay.Move.ReadValue<Vector2>();//每一帧都检测输入系统中的数据 } private void FixedUpdate() {//每隔一段时间(很小的时间)调用一次 Move(); } public void Move(){//控制角色移动的方法 rb.velocity = new Vector2(inputDirection.x*speed*Time.deltaTime,rb.velocity.y);//为角色的速度赋值,velocity代表二位方向上的移动速度 //人物翻转,localScale表示角色的翻转 int faceDir = (int)transform.localScale.x; if(inputDirection.x>0){ faceDir = 1; }else if(inputDirection.x<0){ faceDir = -1; } transform.localScale = new Vector3(faceDir,1,1); } }
C#脚本中有些方法会自动调用,如Awake,Update等,而自己定义的函数不会自动调用,需要在其他地方调用。
修改输入系统,为输入系统添加跳跃按钮配置。点击Actions右侧+添加action,命名为Jump,并为Jump添加Space按钮响应。
在PlayerController 脚本中实现点击space跳跃。
private void Awake() {//这个函数在开始时会调用
rb = GetComponent<Rigidbody2D>();
inputControl = new PlayerInputControl();//实例化PlayerInputControl
inputControl.Gameplay.Jump.started+=Jump;//为按下space按钮添加Jump方法
}
//定义一个jumpForce变量,Jump方法实现
public float jumpForce;
private void Jump(InputAction.CallbackContext context)
{
rb.AddForce(transform.up*jumpForce,ForceMode2D.Impulse);//为角色添加一个瞬间向上的力
}
游戏中角色不能一直跳跃,我们需要给角色跳跃添加一些条件,例如只有在地面上才能跳跃。
我们需要做一个物理碰撞检测,判断角色是否在地面上。
创建一个脚本用于物理检测。
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel.Design; using UnityEngine; public class PhysicsCheck : MonoBehaviour { [Header("check parameter")]//给unity内添加提示信息 public float checkRaduis;//碰撞检测范围 public LayerMask groundLayer;//碰撞检测的图层 public Vector2 bottomOffset;//碰撞检测的偏移 [Header("status")] public bool isGround;//角色是否在地面上 // Update is called once per frame void Update() { Check(); } public void Check() { isGround = Physics2D.OverlapCircle((Vector2)transform.position+bottomOffset,checkRaduis,groundLayer);//碰撞检测函数,参数:监测点坐标,检测范围,碰撞图层 } //用辅助线绘制出检测范围 void OnDrawGizmosSelected() { Gizmos.DrawWireSphere((Vector2)transform.position+bottomOffset,checkRaduis);//参数:监测点,检测范围 } }
脚本就像组件一样,可以挂载到角色上。设置检测范围,碰撞图层等参数
为地面设置图层,这样就可以实时监测角色是否在地面上了。
做完了检测脚本,给角色跳跃添加条件,在角色控制脚本中修改:
private PhysicsCheck physicsCheck;//声明检测变量
private void Awake() {//这个函数在开始时会调用
physicsCheck = GetComponent<PhysicsCheck>();//得到物理检测组件
}
private void Jump(InputAction.CallbackContext context)
{
if(physicsCheck.isGround){//只有当角色在地面上时才能跳跃
rb.AddForce(transform.up*jumpForce,ForceMode2D.Impulse);//为角色添加一个瞬间向上的力
}
}
当角色跳跃时撞到墙上,可能会因为摩擦力而导致黏在墙上,需要修改角色的材质,将角色材质修改成无摩擦力的材质。
在支援文件夹中创建文件夹用于存放物理材质,创建一个2D物理材质。
将它的摩擦力设置为0
将该材质应用到角色上
游戏角色通常有站立动画和移动动画,跳跃动画等,我们需要通过unity的Animation系统来实现角色这些动作的切换。
首先创建一个文件夹来存放Animation文件(包括Animation和Animator Controller文件)。
再创建一个Animator Controller文件.
再选择windows->Animation->Animator查看该动画控制器
再打开Animation
创建新的动画,将对应的动画素材拖入
显示播放速度,可以修改动画播放的速度。
每创建一个Animation,Animator中会出现对应的同名方形框,分别表示该状态下的动画,可以选中删除,删除后可以将文件中的animation拖进animator添加。方形框之间可以转化。选中方形框,右键,点击Make Transition可以连接到其他的动画,表示A动画可以转换到B动画。
在Animator中添加参数,添加的参数可以在条件切换中使用
选中连接线,可以为动画转化设置条件。
这里设置的参数是当x方向上的速度小于0.1时从Run动画切换到Idol动画。根据需要创建动画和转换条件。
创建脚本用于控制动画的切换。
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerAnimation : MonoBehaviour { private Animator animator;//声明一个Animator变量 private Rigidbody2D rb; private void Awake() { animator = GetComponent<Animator>(); rb = GetComponent<Rigidbody2D>(); } private void Update() { SetAnimation(); } public void SetAnimation() { animator.SetFloat("velocityX",Mathf.Abs(rb.velocity.x));//給animator中的velocityX传值来控制动画,Mathf.Abs是取绝对值函数。 } }
使用blend tree更快捷实现animation切换。用角色跳跃行为举例:角色跳跃需要经过起跳,跳起,下落,落地几个阶段。先创建这几个阶段的animation。在animator中创建一个blend tree。
双击选中该blend tree,设置需要混合的animation。跳跃动画切换的条件是y轴方向上的速度变化,需要在animator中添加参数velocityY。
blend tree会根据velocityY的变化切换到对应的animation.
blend tree连接和其他状态的连接方式一样,设置切换条件,编写逻辑代码。
例如从Any State状态切换到Jump(blend tree),切换条件为角色离开地面.
逻辑代码
physicsCheck = GetComponent<PhysicsCheck>();//获取碰撞检测组件
//传递角色参数到animator
public void SetAnimation()
{
animator.SetFloat("velocityY",rb.velocity.y);//跳跃动画切换
animator.SetBool("isGround",physicsCheck.isGround);
}
animator中有两个特殊的自带的状态
Any State:任何状态,从该状态连接到状态A表示任何状态都可以切换到A状态。
Exit:离开该状态,从状态A连接到该状态表示离开状态A。
游戏中会有让角色攻击敌人或者被敌人攻击受伤扣血,现在来实现一下受伤扣血机制。
按照创建player的方法创建一个敌人。敌人添加两个碰撞体,一个用于与地面产生作用让敌人可以站在地面上,一个用于敌人与玩家碰撞产生伤害。当敌人与角色碰撞时我们希望两者可以穿过去,而不是挤在一起。
为敌人和角色添加图层标签,便于控制不同图层的碰撞逻辑。勾选敌人用于产生伤害的碰撞体的Is Trigger,这样在碰撞时可以触发响应事件。
在碰撞体中Layer Overrides->Exclude Layers设置哪些图层不需要触发碰撞检测。
再来编写一些伤害触发的逻辑脚本。
先创立一个脚本用于控制对象属性和受伤的一些机制。角色应该有最大血量和当前血量,当角色被攻击后应该会扣血并触发一个无敌时间。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Attack : MonoBehaviour { public int damage;//单次攻击伤害 public float attackRange;//攻击范围 public float attackRate;//攻击速度 // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } private void OnTriggerStay2D(Collider2D other)//碰撞触发攻击事件,调用碰撞对象的受伤机制 { other.GetComponent<Character>()?.TakeDamage(this);//?判断对象是否有这个方法,有执行,没有不执行 } }
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { [Header("base attrib")] public float maxHP;//最大血量 public float currentHP;//当前血量 public float invulnerableDuration;//受伤无敌时间 private float invulnerableCounter;//无敌时间计时器 public bool invulnerable;//是否处于无敌状态 // Start is called before the first frame update void Start() { currentHP = maxHP; } // Update is called once per frame void Update()//每一帧都检测无敌倒计时,更新无敌状态 { if(invulnerable){//无敌时间倒计时 invulnerableCounter -= Time.deltaTime; if(invulnerableCounter<=0){ invulnerable = false; } } } public void TakeDamage(Attack attacker){//触发受伤 if(invulnerable){//无敌状态不受伤 return; } if(currentHP - attacker.damage > 0){//当前生命值足够扣血 currentHP -= attacker.damage; TriggerInvulnerable(); }else{ //当前生命值不足以扣血直接让生命值为0 currentHP = 0; } } private void TriggerInvulnerable(){//受伤触发无敌 if(!invulnerable){ invulnerable = true; invulnerableCounter = invulnerableDuration; } } }
当A去碰撞B时,A会触发碰撞检测(Attack ->OnTriggerStay2D),
再调用B的伤害处理机制(Character ->TakeDamage),让B扣血。
游戏中当角色被攻击时会有受伤的动画,角色受伤会有角色被击退并且角色闪烁的效果,这可能需要同时播放多个动画。第一个方法我们可以使用多个animation layer来实现。
进入animator,点击layer,创建新的layer.
点击layer右侧的齿轮,weight表示代表该层动画的优先级,数值越大优先级越高。Blending表示动画的方式,是Override是覆盖其他层,Additive是附加到其他层。选择Additive即可。在新的层中创建动画。
还有一个方法。在animation中,修改受伤的动画。点击Add Property,添加Material_Color,就可以为每个关键帧设置图片的rgba(颜色和透明度)。
因为受伤是不是一直存在的,需要创建一个空动画,在不受伤时播放空动画。受伤时从空动画切换到受伤动画,受伤动画播放完之后再回到空动画。为动画转换添加转换条件,创建一个trigger类型参数isHurt,将该参数设置为动画的转换条件。当触发器触发时播放受伤动画。
编写受伤动画播放逻辑。
//PlayerController
private void FixedUpdate() {//每隔一段时间(很小的时间)调用一次
if(!isHurt){//受伤时玩家不能控制角色
Move();
}
}
public void GetHurt(Transform attacker){
isHurt = true;
rb.velocity = Vector2.zero;//受伤后让角色速度归0
Vector2 dir = new Vector2((transform.position.x - attacker.position.x),0).normalized;//获取受伤反弹方向
rb.AddForce(dir * hurtForce,ForceMode2D.Impulse);//向反弹方向施加一个力
}
创建UnityEvent
//Character using UnityEngine.Events;//引用Events库 public UnityEvent<Transform> OnTakeDamage;//注册受伤回调,UnityEvent<想传递的参数(可以没有)> 名字 public void TakeDamage(Attack attacker){//触发受伤 if(invulnerable){//无敌状态不受伤 return; } if(currentHP - attacker.damage > 0){//当前生命值足够扣血 currentHP -= attacker.damage; TriggerInvulnerable(); OnTakeDamage?.Invoke(attacker.transform);//触发受伤响应事件 }else{ //当前生命值不足以扣血直接让生命值为0 currentHP = 0; OnDie?.Invoke(); } }
创建之后unity中会出现该回调。我们可以向该回调中添加方法,当触发该回调时会执行这些方法。
我们在animator选中受伤的动画,点击Add Behaviour创建并添加状态机脚本
打开脚本,里面有预制的几个和动画相关方法,分别是进入动画时执行的方法,动画播放中执行的方法,离开动画时执行的方法。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class HurtAnimation : StateMachineBehaviour { // override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)//当进度该动画时执行 // { // } // override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)//当动画播放中时持续执行的 // { // } override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)//当离开动画时执行 { animator.GetComponent<PlayerController>().isHurt = false; } }
在离开动画时让角色的受伤状态变为false。
用相同的方式创建死亡逻辑。
当角色死亡时我们需要屏蔽玩家操作
//PlayerController
public void PlayerDead(){
isDead = true;
inputControl.Gameplay.Disable();//死亡屏蔽玩家控制角色
}
角色死亡动画只需要播放一次,死亡动画中的Loop Time不要勾选
游戏要实现角色单次攻击时只有一个攻击招式,但连续攻击时会有不同的攻击招式。
和跳跃功能一样,需要现在输入系统添加攻击按钮响应。
在animator中创新一个新的layer用于执行攻击的动画切换。创建三个animation,分别表示连击1,连击2,连击3。当玩家在攻击时继续点击攻击会触发二段攻击,继续攻击触发三段攻击。但当角色不攻击之后就退出攻击动画。所以设计出以下的行为逻辑。为动画切换添加两个条件。角色是否处于攻击状态(bool类型),角色发起攻击触发(trigger类型)。
连击之间的切换不同于其他动画的切换,我们需要在前一个连击快要运行结束时衔接上下一个连击。需要勾选 Has Exit Time,并把离开时间设置为该动画播放完90%。
而切换到离开状态只是需要等播放完。
和受伤动画一样,需要创建状态机脚本,在离开动画时将攻击状态取消,注意三个攻击都需要添加。
攻击逻辑脚本
//PlayerController public bool isAttack; private void Awake() {//这个函数在开始时会调用 rb = GetComponent<Rigidbody2D>(); physicsCheck = GetComponent<PhysicsCheck>();//得到物理检测组件 playerAnimation = GetComponent<PlayerAnimation>(); inputControl.Gameplay.Attack.started += PlayerAttack; } private void PlayerAttack(InputAction.CallbackContext context) { playerAnimation.PlayAttack(); isAttack = true; }
//PlayerAnimation private void Awake() { animator = GetComponent<Animator>(); rb = GetComponent<Rigidbody2D>(); playerController = GetComponent<PlayerController>(); } public void SetAnimation() { animator.SetBool("isAttack",playerController.isAttack);//设置角色是否处于攻击 } public void PlayAttack(){ animator.SetTrigger("attack"); }
//AttackFinish状态机脚本
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.GetComponent<PlayerController>().isAttack = false;
}
创建一个空物体Attack Area挂载在角色下,作为角色的子物体用于攻击碰撞判断,再分别创建三个空对象Attack123挂载在Attack Area下用于三段攻击的碰撞检测判定。分别为Attack123添加Polygon碰撞体,并设置好碰撞范围,将Is Trigger都启用。碰撞检测只需要在角色发出攻击的时候判断,其他时间不需要判断碰撞,可以禁用该碰撞检测。
避免角色也会受到攻击需要在碰撞体中设置碰撞Layer为Enemy。
编辑攻击的动画,为动画添加触发事件。点击Add Property,选择Attack Area->Game Object.Is Active。在关键帧中修改是否启用该Attack。动画中只有一帧是挥出武器打出伤害,只需要在这一帧启用Attack,其他帧禁用即可,将三段攻击都设置好。
游戏中会有敌人,敌人会有多种。如果分别为所有敌人都写一套逻辑会有些繁琐。因为每个敌人都有一些相同的属性行为,比如敌人都要移动,都有血量和攻击力等等,为每个敌人都写一套逻辑有很多逻辑都是重复的。我们可以创建一个敌人的基本逻辑,这个类里面包含所有敌人最基本的逻辑,让所有的敌人都共用这套逻辑。而每个敌人的不同点都写在他们各自的逻辑中。
创建一个敌人基本类Enemy
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { Rigidbody2D rb; protected Animator animator;//只有父类和子类可访问,外部不可访问 [Header("base attrib")] public float normalSpeed;//正常速度 public float chaseSpeed;//追击速度 public float currentSpeed;//当前速度 public Vector3 faceDir;//移动方向 private void Awake() { rb = GetComponent<Rigidbody2D>(); animator = GetComponent<Animator>(); currentSpeed = normalSpeed; } // Start is called before the first frame update void Start() { } // Update is called once per frame private void Update() { faceDir = new Vector3(-transform.localScale.x,0,0);//获取角色的移动方向,//获取角色的移动方向,transform是已经默认添加到类中,不需要额外定义变量去获取Transform组件 } private void FixedUpdate() { Move(); } public virtual void Move(){//将移动设置为虚函数,让子类可以重写 rb.velocity = new Vector2(currentSpeed * faceDir.x * Time.deltaTime,rb.velocity.y);//为敌人添加速度 } }
创建野猪类,让野猪类继承Enemy,因为需要播放移动动画,需要重写父类(enemy)的Move方法。只有父类中用virtual关键字声明的方法,在子类中才能用override关键字重写该方法。
base.Move()表示执行父类中的Move方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boar : Enemy
{
public override void Move()//override重写父类Move函数
{
base.Move();//执行父类函数
animator.SetBool("walk",true);
}
}
我们希望敌人在一定范围内自动来回走动巡逻,遇到墙壁等待一段时间后掉头继续巡逻。
需要给敌人添加左右两个碰撞点作为敌人的碰撞检测。
在PhysicsCheck脚本中添加碰撞检测逻辑,当敌人往右走时我们对右边的碰撞点进行检测,往左走时对左边的碰撞点进行检测。
//PhysicsCheck
public void Check()
{
if(transform.localScale.x>0){
touchRightWall = false;
touchLeftWall = Physics2D.OverlapCircle((Vector2)transform.position + leftOffset,checkRaduis,groundLayer);
}else if(transform.localScale.x<0){
touchLeftWall = false;
touchRightWall = Physics2D.OverlapCircle((Vector2)transform.position + rightOffset,checkRaduis,groundLayer);
}
}
在Enemy中添加角色撞墙原地等待一会再掉头逻辑。创建变量isWait用来记录敌人是否处于等待状态,waitTimeCounter用来记录等待倒计时,waitTime表示等待的时长。当敌人不处于等待状态,并且撞墙的时候,让敌人进入等待状态。TimeCounter方法为等待倒计时,当倒计时为0的时候等待结束,退出等待状态,敌人掉头继续巡逻。
//Enemy private void Update() { faceDir = new Vector3(-transform.localScale.x,0,0);//获取角色的移动方向,transform是已经默认添加到类中,不需要额外定义变量去获取Transform组件 if(!isWait && (physicsCheck.touchLeftWall||physicsCheck.touchRightWall)){ isWait = true; waitTimeCounter = waitTime; animator.SetBool("walk",false); } if(isWait){ TimeCounter(); } } public void TimeCounter(){ waitTimeCounter -= Time.deltaTime; if(waitTimeCounter<=0){ isWait = false; transform.localScale = new Vector3(faceDir.x,1,1); } }
//Boar
public override void Move()//override重写父类Move函数
{
base.Move();//执行父类函数
if(!isWait){
animator.SetBool("walk",true);
}
}
当玩家攻击敌人,敌人会被击退并闪烁。创建敌人受伤动画。为敌人添加受伤状态,当敌人处于受伤状态时不可移动。还需要在敌人受伤时给敌人一个击退效果,并在受伤动画播放完之后让敌人退出受伤状态。
//Enemy public void OnTakeDamage(Transform attackTrans){ attacker = attackTrans; if(attackTrans.position.x > transform.position.x ){//设置敌人方向,让敌人面向角色 transform.localScale = new Vector3(-1,1,1); } if(attackTrans.position.x < transform.position.x){ transform.localScale = new Vector3(1,1,1); } isHurt = true; isWait = false;//退出等待状态 animator.SetTrigger("hurt"); Vector2 dir = new Vector2(transform.position.x-attackTrans.position.x,0).normalized;//设置击退力的方向 rb.velocity = new Vector2(0,rb.velocity.y);//清除敌人速度 StartCoroutine(OnHurt(dir));//执行协程,因为要等到敌人击退之后才能退出受伤状态,所以需要协程来做一个延迟退出受伤状态 } private IEnumerator OnHurt(Vector2 dir){//协程, rb.AddForce(dir * hurtForce,ForceMode2D.Impulse);//为敌人添加一个力 yield return new WaitForSeconds(0.45f);//返回一个时间等待,只有0.45s之后才能继续执行后续代码 isHurt = false; }
死亡动画和受伤类似,但需要在执行动画后将敌人对象销毁。敌人在死亡动画播放时已经是死亡了,此时不应该再与角色有碰撞判定了。所以需要在敌人死亡的时候修改它的layer(Ignore Raycast),并让该layer与player层不产生碰撞交互。
//Enemy
public void OnDie(){
gameObject.layer = 2;//将敌人的layer设置为Ignore Raycast(序号为2)
animator.SetBool("dead",true);//播放死亡动画
isDead = true;//设置死亡状态
}
public void OnDestroyAfterAnimation()//销毁角色
{
Destroy(this.gameObject);
}
点击Edit->Project Settings->Physics 2D,将需要取消的layer交互取消勾选。
为了让角色播放完死亡动画后在销毁,需要在死亡动画最后一帧添加帧事件。
选中最后一帧,右键添加帧事件
设置帧事件,添加写好的销毁方法。
为了让程序逻辑更独立清晰,我们可以创建一个有限状态机来实现敌人状态切换的功能。
先创建一个抽象类BaseState作为基类,该类不继承任何父类。
public abstract class BaseState //abstract关键字表示该类为抽象类
{
protected Enemy currentEnemy;
public abstract void OnEnter(Enemy enemy);//抽象类方法在子类中必须被实现
public abstract void LogicUpdate();
public abstract void PhysicsUpdate();
public abstract void OnExit();
}
再创建具体的敌人状态类,继承BaseState。
实现敌人巡逻状态的逻辑(原来的逻辑删除)
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BoarPatrolState : BaseState { public override void OnEnter(Enemy enemy) { currentEnemy = enemy; } public override void LogicUpdate() { if(!currentEnemy.isWait){ if(!currentEnemy.physicsCheck.isGround || currentEnemy.physicsCheck.touchLeftWall||currentEnemy.physicsCheck.touchRightWall){ currentEnemy.isWait = true; currentEnemy.waitTimeCounter = currentEnemy.waitTime; currentEnemy.animator.SetBool("walk",false); }else{ currentEnemy.animator.SetBool("walk",true); } }else{ currentEnemy.animator.SetBool("walk",false); } } public override void OnExit() { } public override void PhysicsUpdate() { } }
在Enemy中创建状态机对象,并使用。
//Enemy
protected BaseState patrolState;//巡逻状态
private BaseState currentState;//当前状态
protected BaseState chaseState;//追击状态
private void OnEnable()
{
currentState = patrolState;
currentState.OnEnter(this);
}
private void Update()
{
currentState.LogicUpdate();
}
//Boar
protected override void Awake() {//创建巡逻状态机对象
base.Awake();
patrolState = new BoarPatrolState();
}
实现敌人追击逻辑,我想要让敌人看到角色在身前时进入追逐状态,追逐状态下敌人移动速度变快,切撞墙后不做停留直接转身继续追逐。当失去目标一定时间后恢复到巡逻状态。
创建enum脚本枚举NPC状态。
//Enums
public enum NPCState{
Patrol,//巡逻状态
Chase,//追击状态
}
状态切换逻辑
//Enemy
public void SwitchState(NPCState state){//参数为敌人的状态
var newState = state switch{//switch语法糖,如果state是NPCState.Patrol,则newState = patrolState...,如果都不是,则newState = null
NPCState.Patrol => patrolState,
NPCState.Chase => chaseState,
_ => null
};
currentState.OnExit();//退出上一个状态
currentState = newState;
currentState.OnEnter(this);//进入新状态
}
创建敌人追击类
//BoarChaseState using System.Collections; using System.Collections.Generic; using UnityEngine; public class BoarChaseState : BaseState { public override void OnEnter(Enemy enemy) { currentEnemy = enemy; currentEnemy.currentSpeed = currentEnemy.chaseSpeed;//当敌人追击时速度变快 currentEnemy.animator.SetBool("run",true);//切换到追击动画 } public override void LogicUpdate() { if(currentEnemy.lostTarget_timeCounter <= 0){//失去目标,倒计时结束后回到巡逻状态 currentEnemy.SwitchState(NPCState.Patrol); } if(!currentEnemy.physicsCheck.isGround || currentEnemy.physicsCheck.touchLeftWall||currentEnemy.physicsCheck.touchRightWall){//追击状态下撞墙直接转身 currentEnemy.transform.localScale = new Vector3(currentEnemy.faceDir.x,1,1); } } public override void PhysicsUpdate() { } public override void OnExit()//退出状态,退出追击动画 { currentEnemy.animator.SetBool("run",false); } }
敌人检测角色逻辑,和之前检测碰撞逻辑类似
//Enemy
public bool FoundPlayer(){
return Physics2D.BoxCast(transform.position+(Vector3)centerOffset,checkSize,0,faceDir,checkDistance,attackLayer);
}
失去目标倒计时,同时修改一下之前撞墙后等待倒计时逻辑
//撞墙等待倒计时 //Enemy public void TimeCounter(){ if(isWait){//在这里判断等待状态 waitTimeCounter -= Time.deltaTime; if(waitTimeCounter<=0){ isWait = false; transform.localScale = new Vector3(faceDir.x,1,1); } } if(FoundPlayer()){//如果发现角色,重置倒计时 lostTarget_timeCounter = lostTarget_time; } if(!FoundPlayer()&&lostTarget_timeCounter>0){//没发现角色,开始倒计时 lostTarget_timeCounter -= Time.deltaTime; } }
为了方便玩家查看角色状态,通常在界面左上角会有一个显示角色血量或能量的UI。这次我们来创建这个UI。
在Hierarchy界面右键,UI->Canvas创建一个Canvas,然后再Canvas下创建Image。
Canvas是一张画布,我们可以在它上面添加其他UI组件。
我们需要有一张角色的头像。
在Canvas下创建Image,
设置Image相对于Canvas位置到左上角(锚点),并调整好位置。设置头像为角色头像。
角色头像可以用Sprite Editor去裁剪出来,也可以用其他方法。为头像对象创建子对象ImageA,再为A创建子对象ImageB,为ImageB设置角色图片,为Image添加组件Mask并取消勾选Show Mask Graphic。这样会只显示ImageB中与ImageA重叠的部分,只要把角色的头放到这个位置即可。
创建血槽,需要三个部分,第一部分血槽边框,第二部分,血量显示,第三部分,血量减少时的血量缓冲。分别创建三个Image,分别展示这三个部分并把他们组合在一起。(下方的对象会优先展示在前面)。因为第二部分和第三部分需要填充在第一部分中,他们会增加和减少。修改它们的Image Type为水平从左往右填充,修改Fill Amount即可控制填充程度。最后为这三个部分添加一个空父节点,方便管理。用同样的方式创建能量条。
实现角色血量变化。当角色受到伤害时我们角色的血槽也应该减少。
创建代码来控制血槽和能量槽变化。
//PlayerStatBar using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI;//引用UI命名空间 public class PlayerStatBar : MonoBehaviour { public Image healthImage;//血量填充 public Image healthDelayImage;//血量延迟填充 public Image powerImage;//能量填充 private void Update() {//每一帧检测是否需要伤害缓冲 if(healthDelayImage.fillAmount>healthImage.fillAmount){ healthDelayImage.fillAmount -= Time.deltaTime;//逐渐减少填充量直到和真实血量一致 } } public void OnHealthChange(float persentage){//设置血量UI变化 healthImage.fillAmount = persentage; } }
之前已经做好了角色受伤数据方面的逻辑,只需要把血量数据更新到脚本中即可。
使用Scriptable Object来实现数据的传递(可以跨场景,跨对象)。
Scriptable Objec可以用来存储资源文件,因为它是保存在本地的,不用担心游戏跨场景时被销毁。
创建Scriptable Object,先创建一个C#脚本,让它继承ScriptableObject
//CharacterEventSO using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; [CreateAssetMenu(menuName = "Event/CharacterEventSO")]//标识,让该脚本能出现在菜单里 public class CharacterEventSO : ScriptableObject { public UnityAction<Character> OnEventRaised;//定义事件 public void RaiseEvent(Character character){//调用事件 OnEventRaised?.Invoke(character); } }
创建好后可以在Project窗口菜单栏看到,创建该对象。
在 Character脚本中创建UnityEvent,并设置参数。
//Character
public UnityEvent<Character> OnHealthChange;//注册受伤血量变化回调
public void TakeDamage(Attack attacker){//触发受伤
OnHealthChange?.Invoke(this);//受伤时更新血量UI
}
为该回调设置参数
创建一个UI管理器用来管理UI相关的数据调用。
//UIManager using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class UIManager : MonoBehaviour { public PlayerStatBar playerStatBar; [Header("Event listeners")] public CharacterEventSO healthEvent;//创建血量变化事件 private void OnEnable() { healthEvent.OnEventRaised += OnHealthEvent;//添加OnHealthEvent事件 } private void OnDisable() { healthEvent.OnEventRaised -= OnHealthEvent;//删除OnHealthEvent事件 } private void OnHealthEvent(Character character)//获取角色当前的血量与最大血量比,并调用playerStatBar.OnHealthChange,更新血量UI { var persentage = character.currentHP / character.maxHP; playerStatBar.OnHealthChange(persentage); } }
这里的逻辑:当角色受伤时,会调用Character中OnHealthChange,OnHealthChange中设置了CharacterEventSO的RaiseEvent事件,唤醒了OnEventRaised事件,因为UIManager向OnEventRaised中添加了OnHealthEvent事件,OnEventRaised调用了OnHealthEvent,OnHealthEvent调用了PlayerStatBar.OnHealthChange。
逻辑顺序:Character.OnHealthChange -> CharacterEventSO.RaiseEvent ->UIManager.OnHealthEvent -> PlayerStatBar.OnHealthChange
实现血量减少缓冲,只需在每一帧判断血量缓冲和真是血量填充度是否相同,血量缓冲大于真是血量时让血量缓冲减少即可。
//PlayerStatBar
private void Update() {//每一帧检测是否需要伤害缓冲
if(healthDelayImage.fillAmount>healthImage.fillAmount){
healthDelayImage.fillAmount -= Time.deltaTime;//逐渐减少填充量直到和真实血量一致
}
}
当地图足够大,而相机不能覆盖整个地图时,移动角色相机也应该跟着角色移动。直接的想法是将相机绑定在角色身上,角色移动时相机也跟着移动。我们也可以使用插件Cinemachine来方便的完成这个功能。
Windows->Package Manager打开包管理器,搜索Cinemachine并安装它
安装好之后在Hierachy中创建对象是会增加一栏Cinemachine,选择创建2DCamera
Virtual Camera就是创建的相机。设置关键参数,让相机跟随角色。
设置Cinemachine之后画面可能会有一些模糊,可以将Main Camera中的 CinemachineBrain的Update Method设置为Fixed Update。
地图会有边界,我们不希望相机中出现边界之外的场景。可以使用Cinemachine的一些拓展功能
CinemachineConfiner2D:为相机创建一个移动范围,当相机只会在该范围内移动。
Bounding Shape 2D为限制范围,需要添加一个碰撞范围,该范围必须要大于相机范围,否测会出现无法预知的情况。
创建一个空对象命名为Bounds并添加Polygon collider2D组件,将碰撞范围包裹住地图,并勾选Is Trigger。将该对象作为CinemachineConfiner2D的限制范围。将Bounds tag设置为Bounds,编写脚本让CinemachineConfiner2D获取该对象。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Cinemachine; using System;//引用命名空间 public class CameraControl : MonoBehaviour { private CinemachineConfiner2D confiner2D;//碰撞体(限制范围) private void Awake() { confiner2D = GetComponent<CinemachineConfiner2D>(); } private void Start() { GetNewCameraBounds(); } private void GetNewCameraBounds(){ var obj = GameObject.FindGameObjectWithTag("Bounds");//查找tag为Bounds的对象 if(!obj){ return; } confiner2D.m_BoundingShape2D = obj.GetComponent<Collider2D>();//设置限制范围 confiner2D.InvalidateCache();//清掉范围缓存 } }
CinemachinePixelPerfect:对像素渲染的一个修正。
CinemachineImpulseListener:对相机震动监听,可以用来实现相机震动效果。
创建一个空对象,添加组件 CinemachineImpulseSource,该组件提供相机震动效果。
可以运行游戏,点击Invoke来查看效果
我希望让角色在被攻击的时候产生震动效果。方式和角色血量UI变化一样。
编写脚本执行该逻辑。
using UnityEngine;
using UnityEngine.Events;
[CreateAssetMenu(menuName = "Event/VoidEventSO")]
public class VoidEventSO : ScriptableObject
{
public UnityAction OnEventRaised;
public void RaiseEvent(){
OnEventRaised?.Invoke();
}
}
using System.Collections; using System.Collections.Generic; using UnityEngine; using Cinemachine;//引用命名空间 public class CameraControl : MonoBehaviour { private CinemachineConfiner2D confiner2D;//碰撞体(限制范围) public CinemachineImpulseSource cinemachineImpulseSource; public VoidEventSO cameraShakeEvent; private void Awake() { confiner2D = GetComponent<CinemachineConfiner2D>(); } private void OnEnable() { cameraShakeEvent.OnEventRaised += OnCameraShakeEvent; } private void OnDisable() { cameraShakeEvent.OnEventRaised -= OnCameraShakeEvent; } private void OnCameraShakeEvent() { cinemachineImpulseSource.GenerateImpulse();//产生震动 } private void Start() { GetNewCameraBounds(); } private void GetNewCameraBounds(){ var obj = GameObject.FindGameObjectWithTag("Bounds");//查找tag为Bounds的对象 if(!obj){ return; } confiner2D.m_BoundingShape2D = obj.GetComponent<Collider2D>();//设置限制范围 confiner2D.InvalidateCache();//清掉范围缓存 } }
音乐是游戏的一部分,让我们来看看怎么向游戏中添加音乐。
unity的音乐播放组件主要有 Audio Source(声音产生组件,像嗓子) 和 Audio Listener(声音接受组件,像耳朵)。
先创建一个AudioManager用来管理音乐系统。向AudioManager添加两个Audio Source,分别用来控制游戏音效和背景音乐。
我想要角色在攻击播放音效,游戏开始时播放背景音乐。
同角色血量UI变化,设置触发事件。
//编写音乐播放ScriptableObject using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; [CreateAssetMenu(menuName = "Event/PlayAudioEventOS")] public class PlayAudioEventSO : ScriptableObject { public UnityAction<AudioClip> OnEventRaised; public void RaiseEvent(AudioClip audioClip){ OnEventRaised?.Invoke(audioClip); } }
//设置音乐播放事件触发脚本 //AudioDefination using System.Collections; using System.Collections.Generic; using UnityEngine; public class AudioDefination : MonoBehaviour { public PlayAudioEventSO playAudioEvent; public AudioClip audioClip; public bool playOnEnble; private void OnEnable() { if(playOnEnble){ PlayAudioClip(); } } public void PlayAudioClip(){ playAudioEvent.OnEventRaised(audioClip); } }
//AudioManager using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio;//引用命名空间 public class AudioManager : MonoBehaviour { [Header("listener")] public PlayAudioEventSO FXEvent; public PlayAudioEventSO BGMEvent; [Header("Audio Source")] public AudioSource BGMSource;//背景音乐音源 public AudioSource FXSource;//音效音源 private void OnEnable() { FXEvent.OnEventRaised += OnFXEvent; BGMEvent.OnEventRaised += OnBGMEvent; } private void OnDisable() { FXEvent.OnEventRaised -= OnFXEvent; BGMEvent.OnEventRaised -= OnBGMEvent; } private void OnBGMEvent(AudioClip clip) { BGMSource.clip = clip;//设置背景音乐 BGMSource.Play();//播放背景音乐 } private void OnFXEvent(AudioClip clip) { FXSource.clip = clip;//设置音效音源 FXSource.Play();//播放音效 } }
将AudioDefination 挂载到三个Attack对象上来实现攻击音效,之前已经写过Attack逻辑,只有当角色攻击时才激活Attack,不攻击时Attack是禁用的,并设置音源文件。
创建一个空对象挂载AudioDefination 用来播放BGM。
在project窗口,Assert目录下创建一个Audio Mixer用来管理各个音乐的播放设置。
在Mixers中Groups右键+可以创建子设备,子设备单独控制该输出下的音乐,Master可控制所有子设备的音乐(音量大小等)。
创建了子设备后,可以在Audio Source的Output中选择子设备来输出该音乐。
把物体A加入到一个 sorting layer1,把物体B加入到sorting layer2,在sorting layer中把sorting layer1放在sorting layer2下面,A就会出现在B的前面了。
sorting layer中越在下面的layer会显示在更前面。
把这两个碰撞体叫加到不同的layer,然后在 Edit --> Project Setting -->Physics 2D -->layer collision Matrix中把不需要碰撞的两个layer取消即可。
把这两个物体加入到两个不同的图层,设置两个图层不会相撞之后这两个图层上的物体就不会产生碰撞了。
为了实现NPC随机触发对白,设置了一个简单的随机方法。
获取随机数可以用System命名空间下的类 Random ,Random是一个根据触发时间来产生随机数的类。Random还可以带参数实例化,可以自己设定触发种子。注意UnityEngine命名空间下也有一个Random类,这两个类不一样同时引用这两个命名空间需要指明该类来自哪个命名空间。
private int GetRoundNum()
{
int x;
Random random = new Random();
x = random.Next(0, 6);
return x;
}
单例模式 单例对象的类只有一个对象,并提供让外部访问该类的功能。单例模式又分为懒汉式和饿汉式。懒汉式只有在使用到该类实例时才会实例化(线程不安全),饿汉式则在定义类时就已经实例化了。
可以使用单例模式设计GameManager。
创建一个GameManager类实现游戏数据的管理。GameManager接管了游戏从启动到关闭过程中的数据,包括场景中的物品数量,角色的血量等等。此外,游戏中进行的操作也通过GameManager管理实现,如角色死亡,收集物品。因为GameManager是全程跟随的,它可以实现数据在关卡之间的继承。
可以为梯子添加一个碰撞体,并把碰撞体设为is Trigger (让此碰撞体成为一个开关)。当角色碰到梯子时,触发Trigger,让角色进入一个可爬梯子的状态(可用一个bool变量控制)在可爬梯子状态下且此时角色处于站立不动状态时(防止角色在跳跃或者跑动状态也能爬梯子)按下上下键可以转换到爬梯子状态,爬梯子状态下可以上下左右移动(左右移动会脱离梯子,此时会离开爬梯子状态),但不能进行跳跃下蹲等操作。注意Trigger只有在碰撞的一瞬间能触发,所以不要把收听指令(读取玩家操作)和判断角色状态(角色进入梯子范围这一时刻一般不处于站立不动状态)写OnTriggerEnter2D方法中,最好放在FixedUpdate函数中。
1.PlayerPrefs
PlayerPrefs是Unity3d提供了一个用于数据本地持久化保存与读取的类(保存的数据会存储下来,即使停止执行项目,数据仍然存在,下次运行依然可以加载这些数据。数据以key-value的形式将数据保存在本地,然后在代码中可以写入、读取、更新数据。
2.序列化与反序列化
序列化是把Unity中的对象转化为其他可以存储在本地的数据类型的过程,反序列化则是把这些本地存储的数据转化回Unity中的对象的过程。
序列化存储方式有三种,二进制存储,json和XML。
二进制序列化:定义一个保存类Save,把要存储的数据都在类中声明,在要保存时把数据先记录在Save类对象中,再把Save序列化为二进制数据写入文件中。加载时把文件中的数据反序列化到Save类对象中,再把Save对象中的数据加载到游戏中。二进制序列化保存下来的数据是一些二进制数据,不利于阅读,另外兼容性也不好。
Json序列化: Json序列化数据数据为key-value的形式,可读性更好;
XML序列化:
可以在另一个场景设置一个门,把门添加一个脚本,把门的位置信息传给角色,让角色出现在门的位置。这个方法不仅可以顺便实现角色进入下一关,也可以实现角色回到上一个关卡。
两种方法
1.把角色的位置信息传给新场景中的摄像头,让摄像头的位置与角色同步,注意这两者要一直保持一致,所以更新函数要一直调用。
2.在新场景中设立一个角色替身,让新场景的摄像头跟踪替身,让替身跟踪角色。(因为一开始新场景没有角色这个对象,所以不能让摄像头跟踪角色)。
在Canvas的Inspector窗口中把Canvas中的设置Sort Order,数值越大越显示在前面。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。