Unity3D制作巡逻兵小游戏

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/SquirrelYuyu/article/details/80287860

Github上的项目代码和视频链接

一、主体代码架构说明

作用
GameController 场记,加载资源,管理得分和游戏状态
PatrolFactory 巡逻兵工厂
PatrolEventManager 事件发布者(抓捕成功和逃跑成功的事件)
PlayerController 玩家控制器,负责玩家的移动、碰撞检测、动画调用
PatrolController 巡逻兵控制器,负责检测巡逻兵的状态,动画调用
PatrolBody 检测巡逻兵的近距离碰撞,并告知PatrolController
PatrolAction 巡逻兵动作
PatrolActionManager 巡逻兵动作管理器
UserGUI 接收用户输入,传递给场记;显示得分

其中GameController.csUserGui.cs挂载在Main Camera上。

其余代码为基类或接口,和门面模式、动作管理器模式等有关,在此不作介绍。


二、发布与订阅模式

本周使用了发布与订阅模式。

相关类图

发布与订阅模式的相关类图

C#中的实现:delegate委托类型和event事件机制

在C#中,可以通过delegate委托类型和event事件机制来实现。

就我个人的粗浅理解来看,事件相当于一个列表,它可以存放多个有类似的签名、定义在不同类内的方法。定义某个委托类型,并用它创建事件,就是给这个列表指定方法的签名类型。这些方法内部可以有不同的实现/事件处理。

比如说,有一个要求,篮子里的苹果必须是绿色的。苹果是方法,“绿色”是方法的签名,这个要求是一个委托。由这个要求指定的篮子,就是由这个委托定义的事件。深绿色、浅绿色、黄绿色等等,就是加到这个事件上的不同的事件处理程序了。它们有类似的签名(“绿色”),但是有不同的方法体(颜色的深浅等)。

那么,在C#里,delegate相当于以上设计模式类图里的接口AttackHandle,event相当于Subject下的List< AttackHandle >。包含这个event的类就是Publisher,往这个event上添加自己的事件处理程序的类,就是Subscriber.

意义

那么这种模式有什么意义呢?我觉得有以下几点好处:

  • 在有多个类需要处理某个事件时,可以只用一个事件就通知到它们,让它们各自作出反应,而不必在这些类里分别检测事件并处理。这样可以将事件源与事件处理者解耦。
  • 允许有不同的处理方法。

在本次游戏中的具体应用

PatrolEventManager.cs 发布者

public class PatrolEventManager : MonoBehaviour {
    public static PatrolEventManager instance;
    public delegate void HuntAction();              //巡逻兵抓获成功
    public static event HuntAction OnHuntAction;
    public delegate void FleeAction();              //玩家逃跑成功
    public static event FleeAction OnFleeAction;

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {

    }

    public void flee()
    {
        if (OnFleeAction != null)
        {
            OnFleeAction();
        }
    }

    public void hunt()
    {
        if (OnHuntAction != null)
        {
            OnHuntAction();
        }
    }

    public static void Hunt()
    {
        instance.hunt();
    }

    public static void Flee()
    {
        instance.flee();
    }
}

GameController.cs 订阅者

//注册事件处理程序
public void OnEnable()
{
  PatrolEventManager.OnHuntAction += hunt;
  PatrolEventManager.OnFleeAction += flee;
}

//注销事件处理程序
public void OnDisable()
{
  PatrolEventManager.OnHuntAction -= hunt;
  PatrolEventManager.OnFleeAction -= flee;
}

//玩家成功逃脱后的处理:加分
void flee()
{
  score += 5;
}

//玩家被抓到后的处理:结束游戏
void hunt()
{
  gameState = "Game over!";
  recycleAll();
}

三、游戏主要行为分析

窃以为,本次游戏的难点在于角色的行为,以及相关碰撞体/触发器的使用。

1. 巡逻兵的行为

  • 在遇到障碍物(包括别的角色)时不弹开,也不穿过。
  • 在遇到墙时转向,继续巡逻。
  • 在玩家进入其追捕范围时,追捕玩家。

与其他物体/角色相遇的处理

巡逻兵需要两个Collider,一个是Sphere Collider,挂在巡逻兵模型上,范围较大,用于感应玩家。感应到后,若玩家在当前区域,则开始追击。

相关的检测代码写在PatrolController.cs里:

private void OnTriggerEnter(Collider other)
{
  //玩家在当前区域,则追击
  if (isInZone(other.transform) && other.tag.Equals("Player"))
  {
    player = other.transform;
    gameObject.GetComponent<Animator>().SetBool("ToAttack", true);
  }
}

private void OnTriggerExit(Collider other)
{
  //玩家在当前区域但已离开追捕范围,则追捕失败,玩家得分
  if (isInZone(other.transform) && other.tag.Equals("Player"))
  {
    player = null;
    gameObject.GetComponent<Animator>().SetBool("ToAttack", false);
    PatrolEventManager.Flee();
  }
}

void Update () {
  //...
  if(isHunting && !isInZone(player))  //检测追击过程中玩家离开当前区域的情况
  {
    player = null;
    gameObject.GetComponent<Animator>().SetBool("ToAttack", false);
    PatrolEventManager.Flee();
  }
}

另一个是Box Collider,挂在巡逻兵模型下的空的子物体上,范围较小,用于检测巡逻兵将要撞上其他物体的情况。相关的检测代码写在PatrolBody.cs里:

//检测近距离遇上其他物体的情况
private void OnTriggerEnter(Collider other)
{
  if (other.tag.Equals("wall"))
  {
    patrolController.borderIsFront = true;
  }
  else if (other.tag.Equals("Player"))
  {
    gameObject.transform.parent.GetComponent<PatrolController>().playerIsFront = true;
  }
}

//离开其他物体
private void OnTriggerExit(Collider other)
{
  if (other.tag.Equals("wall"))
  {
    gameObject.transform.parent.GetComponent<PatrolController>().borderIsFront = false;
  }
  else if (other.tag.Equals("Player"))
  {
    gameObject.transform.parent.GetComponent<PatrolController>().playerIsFront = false;
  }
}

相关动作:行走,转向

这部分代码主要由PatrolAction.cs实现,PatrolController.cs提供相关的检测和标志。

//PatrolAction.cs

//每次遇上区域边界后的旋转角度
public int rotateAngle;

//检测巡逻兵状态,为PatrolAction提供改变的依据
private PatrolController patrolController;

//..........

public override void Update () {
  if (destroy)
  {
    return;
  }
  if (patrolController.borderIsFront) //遇上边界则转向
  {
    gameobject.transform.Rotate(gameobject.transform.up, rotateAngle);
    patrolController.borderIsFront = true;
  }
  else if (patrolController.playerIsFront)    //遇上玩家,不移动,举枪攻击玩家
  {
    return;
  }
  else if (patrolController.isHunting)    //正在追击,则目标是玩家,要向着玩家移动
  {
    gameobject.transform.LookAt(patrolController.player);
    gameobject.transform.position = Vector3.MoveTowards(gameobject.transform.position, patrolController.player.position, 1.0f * Time.deltaTime);
  }
  else
  {   //以上情况都不是,进行普通的巡逻
    gameobject.transform.Translate(Vector3.forward * 1.0f * Time.deltaTime);
  }
}

其实可以把PatrolAction定义为几个更简单的动作,在动作结束时调用动作管理器的回调接口来告诉动作管理器,下一个动作应该怎么定义。但是为了方便,就将不同的具体动作写在同一个Action里来处理了。

  • 不能离开所在的区域。(避免巡逻兵从墙的缺口进入其他领域)
  • 只能在玩家进入所在的区域时追捕,不能隔着墙追捕玩家。

因为构建的地图坐标很有规律,所以使用坐标上下限来规定巡逻兵的巡逻区域。

PatrolController.cs

//巡逻区域的坐标范围
private float xmin, xmax, zmin, zmax;

void Start () {
  patrol = gameObject.transform;
  xmin = (int)(patrol.position.x / 5) * 5 + 0.2f;
  xmax = (int)(patrol.position.x / 5 + 1) * 5 - 0.2f;
  zmin = (int)(patrol.position.z / 5) * 5 + 0.2f;
  zmax = (int)(patrol.position.z / 5 + 1) * 5 - 0.2f;
}

当感应到玩家时,用以下方法来检测玩家是否位于当前区域。这个方法也用在Update()里以检测巡逻兵是否要走出当前区域。

//用于检查某些角色是否在当前巡逻区域
private bool isInZone(Transform role)
{
  return zmin <= role.position.z && role.position.z <= zmax && xmin <= role.position.x && role.position.x <= xmax;
}

void Update () {
  if(!isInZone(patrol))   //若巡逻兵走出了范围,则标记borderIsFront为true,将会通知PatrolAction来调整方向
  {
    //...........
  }
  if(isHunting && !isInZone(player))  //检测追击过程中玩家离开当前区域的情况
  {
    //...........
  }
}
  • 追上玩家时,杀死玩家。

在追捕过程中,会调用Shoot动画。

gameObject.GetComponent<Animator>().SetBool("ToAttack", true);

2. 玩家行为

这部分的相关代码写在PlayerController.cs里。

  • 由用户用方向键控制,按哪个方向键就沿哪个方向行走。

UserGUI.cs检测用户输入,调用GameController的movePlayer(),GameController的movePlayer()又调用PlayerController的movePlayer().

//移动玩家
public void movePlayer(float hor, float ver)
{
  if (isDead) return;
  if (ver != 0)
  {
    //如果玩家不是在奔跑,则调用奔跑的动画
    if (!playerAnimator.GetBool("IsRunning")) playerAnimator.SetBool("IsRunning", true);
    if (ver > 0)
    {
      player.forward = Vector3.forward;
      if (IsObstacleFront()) return; //如果玩家遇上了障碍,则不移动
      player.Translate(new Vector3(0, 0, ver));
    }
    else
    {
      player.forward = Vector3.back;
      if (IsObstacleFront()) return;
      player.Translate(new Vector3(0, 0, -ver));
    }
  }
  else if (hor != 0)
  {
    if (!playerAnimator.GetBool("IsRunning")) playerAnimator.SetBool("IsRunning", true);
    if (hor > 0)
    {
      player.forward = Vector3.right;
      if (IsObstacleFront()) return;
      player.Translate(new Vector3(0, 0, hor));
    }
    else
    {
      player.forward = Vector3.left;
      if (IsObstacleFront()) return;
      player.Translate(new Vector3(0, 0, -hor));
    }
  }
  else playerAnimator.SetBool("IsRunning", false);    //没有移动,则不奔跑
}
  • 在遇到障碍物(包括别的角色)时不弹开,也不穿过。

  • 在遇到墙时,只要用户不改变所按的方向键,就会一直在墙前。

玩家模型加了一个Box Collider,勾选了IsTrigger.

(PS:如果Box Collider的位置或大小不够好,玩家遇到墙时,因为待机动画本身的特性,头会撞进墙……)

在OnTriggerEnter()和OnTriggerExit()中会检测障碍物。

在movePlayer()中,会在确定玩家方向后检测障碍物是否在前方,是的话则不移动玩家。

//遇到的其他物体/角色
private Transform obstacle = null;
//以什么方向遇上障碍物
private Vector3 obstacleDirection = Vector3.zero;

//是否正对着障碍物
private bool IsObstacleFront()
{
  return obstacleDirection == player.forward;
}

//检测是否遇上障碍
private void OnTriggerEnter(Collider other)
{
  if (other.tag.Equals("wall"))
  {
    obstacleDirection = player.forward;
  }
  else if (other.tag.Equals("PatrolBody"))    //遇上巡逻兵,死亡
  {
    isDead = true;
    playerAnimator.SetBool("IsDead", true);
  }
}

//离开障碍
private void OnTriggerExit(Collider other)
{
  obstacleDirection = Vector3.zero;
}

//movePlayer()中
if (IsObstacleFront()) return; //如果玩家遇上了障碍,则不移动
  • 被巡逻兵追上则死亡。

设置Animator的布尔类型的变量IsDead为true,播放Dead动画。

private void OnTriggerEnter(Collider other)
{
  if (other.tag.Equals("wall"))
  {
    obstacleDirection = player.forward;
  }
  else if (other.tag.Equals("PatrolBody"))    //遇上巡逻兵,死亡
  {
    isDead = true;
    playerAnimator.SetBool("IsDead", true);
  }
}

3. 其他道具

墙砖加上了运动学刚体,以及勾选了IsTrigger的Box Collider。

墙砖模型:Asset Store, Destructible Wall Generator

巡逻兵模型:Asset Store, Toony Tiny WW1 Soldiers D.

玩家模型:Asset Store, SD Martial Arts Girl Xia-Chan

展开阅读全文

没有更多推荐了,返回首页