目录
一、怪物的移动
之前我们向游戏中添加了敌人,现在要努力让敌人动起来,首先我们之前使用的是方形的碰撞器,为了防止怪物卡住我们改用圆形的。
然后我们为青蛙添加一个新的代码组件,首先和之前实现玩家的移动是一样的我们先要获得敌人的刚体,然后我们要想办法实现敌人“巡逻”的功能,我的想法是就像角色我们可以给它添加一个变量来判断是否到达地面,那么我们可以加入两个变量来担当青蛙左右巡逻的边界点,这样我在制作完成后只需要把青蛙拖入预制文件夹以后使用的时候就无需编写代码,只需要调整左右距离就可以
public Rigidbody2D rb;
public Transform leftpoint; //路径左点
public Transform rightpoint; //路径右点
定义完所有变量后在unity的界面把对应的东西拖进来
因为青蛙一开始面向左边,为了可以控制青蛙的朝向所以我们增添了一个变量(游戏角色不需要是因为可以通过输入的方向来判断)
private bool Faceleft = true;
然后我们把代码补充完整
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_Frog : MonoBehaviour
{
public Rigidbody2D rb;
public Transform leftpoint; //路径左点
public Transform rightpoint; //路径右点
public float speed; //移动速度
private bool Faceleft = true;
void Update()
{
Movement();
}
void Movement()
{
if(Faceleft) //向左移动
{
rb.velocity = new Vector2(-speed, rb.velocity.y);
if(transform.position.x < leftpoint.position.x)
{
transform.localScale = new Vector3(-1, 1, 1);
Faceleft = false;
}
}
else //向右移动
{
rb.velocity = new Vector2(speed, rb.velocity.y);
if (transform.position.x > rightpoint.position.x)
{
transform.localScale = new Vector3(1, 1, 1);
Faceleft = true;
}
}
}
}
但很快我们就发现了问题子物体随着父物体的移动会一起移动,那么我们可以选择在游戏启动后与子物体解绑或者记录下数值后销毁,也可以选择直接把两边的数值输入后,把子物体删掉了,这样的坏处是,设置的每个敌人都需要重新编写代码的巡逻边界,而且每个敌人都需要新建代码(代码不能复用)这样我们就实现了敌人移动的代码实现。
二、怪物的动画
青蛙的动画分为两种分别是站立和走动,我们现在需要给青蛙加上走动的效果(青蛙是跳起和落下)首先和之前一样把这两个动画加到青蛙的身上
加入后我们尝试连接好动画的逻辑,然后创建了用于判断状态转换的条件
然后我们重新改写代码,首先和玩家一样,只有在地面的时候才可以跳起来,我们按照碰撞体是否和地面图层相碰来识别,同理我们用这个思路修改了玩家跳跃的代码。
完整代码如下所示
void Movement()
{
if(Faceleft) //向左移动
{
if(coll.IsTouchingLayers(ground))
{
rb.velocity = new Vector2(-speed, jumpforce);
anim.SetBool("jumping", true);
}
if(transform.position.x < 2.5)
{
transform.localScale = new Vector3(-1, 1, 1);
Faceleft = false;
}
}
else //向右移动
{
if (coll.IsTouchingLayers(ground))
{
rb.velocity = new Vector2(speed, jumpforce);
anim.SetBool("jumping", true);
}
if (transform.position.x > 8.5)
{
transform.localScale = new Vector3(1, 1, 1);
Faceleft = true;
}
}
}
这里分别写了向左移动和向右移动,然后通过是否在地面来进入跳跃状态并跳跃,但此时因为青蛙不能进入下落状态。所以我们要用到新的功能:
选择idle的动画,然后添加一个事件在动画结束的地方,这样我们发现我们可以在这里调用我们写过的函数(比如movement()),这样我们可以在在每个跳跃中添入一个完整的idel状态,此时注意一定要把movement函数从idel函数中去掉,让这个函数在每次idel后自动调用
然后我们继续更改代码,我们按照游戏角色那样添加一个动画切换函数,然后在update中调用
void SwitchAnim() //动画切换函数
{
if(anim.GetBool("jumping"))
{
if(rb.velocity.y < 0.1)
{
anim.SetBool("jumping", false);
anim.SetBool("falling", true);
}
}
if (coll.IsTouchingLayers(ground))
{
anim.SetBool("falling", false);
}
}
至此我们的敌人就实现了巡逻功能和动画切换。
三、实现消灭动画、类的初探。
首先我按照之前的过程把素材库里的其他两个敌人添加进来,一个是老鼠是比较简单的这里就不在赘述,加入各种组件和代码后就可以实现。
注意这里如果使用直接输入位置的方法来实现则一定要用角色来定位位置的数字否则就会出错
然后我们尝试加入老鹰,老鹰和前两者的不同之处在于老鹰是一个飞行的怪物,我们如果在刚体的界面设置了重力会导致它掉下来,其他地方和别的怪物并没有什么区别。
然后这里我对所有敌人的代码做了修改为了方便预制,制作更多敌人,我把直接直接输入值的方法改为了左右放两个空的子对象分别表示左右边界
然后在代码中这样书写,首先通过rightpoint和leftpoint来获取我们刚才创建的两个边界物体,然后通过right和left分别获得坐标,然后为了游戏中运行的性能我们选择直接删除这两个空对象,然后我们只需要把left和right分别写到移动函数的边界判断条件里面就可以了,这样以后我们在制作其他怪物的时候只需要拖出预制的组件然后调整left和right的位置就可以了
public Transform rightpoint, leftpoint;
private float right, left;
void Start()
{
right = rightpoint.position.x;
left = leftpoint.position.x;
Destroy(rightpoint.gameObject);
Destroy(leftpoint.gameObject);
}
接下来是敌人消灭的动画实现,之所以把这个动画单独出来是因为,每个敌人的消灭动画都是一样的,这种情况下为每个敌人都做一个动画是比较麻烦的,所以我们用别的方法实现。
首先我尝试给青蛙添加一个消灭动画,和之前的步骤是一样的,添加过这个动画后我们需要在动画器中写一下动画逻辑(any State到死亡状态)这里面有一个不一样的就是我们把状态转移条件设为trigger(trigger的条件是没有true或者false的)
那么我们如何触发呢?但是我们现在发现一个问题,我们消灭敌人的函数是在角色的脚本中写的,我们是没有办法调用青蛙的死亡动画效果的,所以我们需要一个新方法
我们可以看到这是青蛙脚本的函数的类,如果我们生成了一个该类的对象不就可以调用该类的函数了?我们按照这个思路继续走下去。
Enemy_Frog frog = collision.gameObject.GetComponent<Enemy_Frog>();
我们在消灭角色脚本的消灭怪物的函数中加了这样一句代码,这句代码的意思是,创建了一个Enemy_Frog的对象名字叫frog,然后我们可以通过这个对象调用里面所有的组件是由函数GetComponent<Enemy_Frog>()实现的,但是为了可以销毁所以我们到frog的脚本中写一个公开的函数
public void Death()
{
anim.SetTrigger("deathing");
Destroy(gameObject);
}
然后在玩家代码界面调用这个函数
frog.Death();
但是这样我们发现我们销毁了这个实体后死亡动画没有播放,所以我们用上面用过的动画事件,我们在死亡动画的最后一帧调用一个函数,然后我们重新写一下frog的函数
代码的逻辑是说我们会先调用Death函数,然后播放死亡动画,死亡动画结束后会调用Destory函数销毁物体
public void Death()
{
anim.SetTrigger("deathing");
}
void Destory()
{
Destroy(gameObject);
}
然后我发现了一个bug就是在死亡动画的播放期间,该物体还是被视作一个碰撞体依然可以触发新的死亡动画,所以我在Death里加一行代码在第一次消灭的时候就关闭碰撞体,这样避免了玩家反复跳跃的bug和死亡动画播放多次但是这样会导致一个新的bug就是没有碰撞体的物体会直接掉出地图
coll.enabled = false;
如果我们不想要这个效果就需要锁定刚体的位置,所以我们在Death()中在添加一行代码冻结它的位置
rb.constraints = RigidbodyConstraints2D.FreezePosition;
这样我们就完成了一个敌人死亡动画,那么我们以后可能会实现很多种怪物,我们每个怪物都要写一遍这个过程吗?
我们要用新的方法实现,回到代码我们可以发现每个敌人都是类MonoBehaviour的子类(类的继承)
首先我们新建一个叫Enemy的脚本,我们想要让这个脚本成为所有敌人的父类,我们把所有敌人的脚本父类修改为Enemy,然后写好Enemy的代码,关键字protected是在继承中使用的,virtual会允许子集重新编写父级的函数
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
protected Animator anim;
protected virtual void Start()
{
anim = GetComponent<Animator>();
}
}
然后因为我们已经在父级中定义了Animator所以我们就不需要在子集中定义了,我们可以直接删掉,但是删除后我们原本使用的地方都不能生效了,所以为了既可以使用子集又可以使用父集我们这里给Start函数加上protect关键字,而子集需要对Start做如下的变化这样就实现了虚函数和函数的重写。
然后我们可以把死亡动画和销毁的函数搬到父类里面【注意为了可以在类外调用需要加上public的关键字】
然后发现我们搬过去后无法识别到刚体和碰撞体(因为父类没有定义)所以我们在父类初始化这些变量最后父类Enemy的代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
protected Animator anim;
protected Rigidbody2D rb;
protected Collider2D coll;
protected virtual void Start()
{
anim = GetComponent<Animator>();
rb = GetComponent<Rigidbody2D>();
coll = GetComponent<Collider2D>();
}
public void Death()
{
rb.constraints = RigidbodyConstraints2D.FreezePosition;
coll.enabled = false;
anim.SetTrigger("deathing");
}
public void Destory()
{
Destroy(gameObject);
}
}
然后我们把刚才创建角色脚本里创建对象的代码改为创建一个父类脚本,运行测试没有发现别的问题。
Enemy enemy = collision.gameObject.GetComponent<Enemy>();
其他敌人的制作过程是一样的这里不再赘述,注意这里还是要给每个敌人添加动画效果。