一、主体代码架构说明
类 | 作用 |
---|---|
GameController | 场记,加载资源,管理得分和游戏状态 |
PatrolFactory | 巡逻兵工厂 |
PatrolEventManager | 事件发布者(抓捕成功和逃跑成功的事件) |
PlayerController | 玩家控制器,负责玩家的移动、碰撞检测、动画调用 |
PatrolController | 巡逻兵控制器,负责检测巡逻兵的状态,动画调用 |
PatrolBody | 检测巡逻兵的近距离碰撞,并告知PatrolController |
PatrolAction | 巡逻兵动作 |
PatrolActionManager | 巡逻兵动作管理器 |
UserGUI | 接收用户输入,传递给场记;显示得分 |
其中GameController.cs和UserGui.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