3D Computer Game Programming-Note 7
1、智能巡逻兵
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
- 使用工厂模式生产巡逻兵
【结构设计】
使用订阅与发布模式传消息,即有一个事件发布者来发布不同类型的事件消息,有多个订阅者可以关注该发布者,当事件发布者触发事件通知时,订阅者会第一时间接收到消息,进而触发其他相关事件。
设计程序的 UML 图:
【编程实现】
注:本博客重点讲述动画状态机和订阅/发布模式,代码细节参见🔗github
- GameEventManager.cs: 使用事件——代理机制,
delegate
关键字定义了关于得分和游戏结果事件的代理类型,同时申明两个静态变量ScoreChange
和GameoverChange
,当它们分别被调用时,代理事件会被触发,订阅者从代理事件那里得到消息
public class GameEventManager : MonoBehaviour {
public delegate void ScoreEvent();
public static event ScoreEvent ScoreChange;
public delegate void GameoverEvent();
public static event GameoverEvent GameoverChange;
public void PlayerEscape() {
if (ScoreChange != null)
{
ScoreChange();
}
}
public void PlayerGameover() {
if (GameoverChange != null)
{
GameoverChange();
}
}
}
- FirstController.cs:
FirstController
为订阅者,加赋值表示订阅,减赋值表示取消订阅,这样,在订阅后FirstController
可以随时关注到游戏事件的动态变化,将信息的传递和控制进一步分离出来
public class FirstController : MonoBehaviour, IUserAction, ISceneController {
/* ... */
void OnEnable() {
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameoverChange += Gameover;
}
void OnDisable() {
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameoverChange -= Gameover;
}
void AddScore() {
recorder.AddScore();
}
public void Gameover() {
game_over = true;
patrol_factory.StopPatrol();
action_manager.DestroyAllAction();
}
}
- PatrolCollider.cs: 通过实例化事件发布类的一个实例发布消息,所有订阅了该事件的订阅者都会接收到这个消息
public class PatrolCollider : MonoBehaviour {
/* ... */
void OnCollisionEnter(Collision obj) {
if (obj.gameObject.tag == "Player") {
obj.gameObject.GetComponent<Animator>().SetTrigger("death");
this.GetComponent<Animator>().SetTrigger("shoot");
Singleton<GameEventManager>.Instance.PlayerGameover();
}
}
}
- GoPatrolAction.cs: 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址,即每次确定下一个目标位置,用自己当前位置为原点计算;当巡逻兵碰撞到障碍物(墙壁时),自动选择下一个点为目标
public class GoPatrolAction : SSAction {
private enum Dirction { EAST, NORTH, WEST, SOUTH };
/* ... */
void Gopatrol() {
if (move_sign) {
switch (dirction) {
case Dirction.EAST:
pos_x -= move_length;
break;
case Dirction.NORTH:
pos_z += move_length;
break;
case Dirction.WEST:
pos_x += move_length;
break;
case Dirction.SOUTH:
pos_z -= move_length;
break;
}
move_sign = false;
}
this.transform.LookAt(new Vector3(pos_x, 0, pos_z));
float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z));
if (distance > 0.9) {
transform.position = Vector3.MoveTowards(this.transform.position, new Vector3(pos_x, 0, pos_z), move_speed * Time.deltaTime);
}
else {
dirction = dirction + 1;
if (dirction > Dirction.SOUTH)
{
dirction = Dirction.EAST;
}
move_sign = true;
}
}
}
- PatrolZoneCollider: 巡逻兵在设定范围内感知到玩家时,将玩家设置为追击目标和碰撞检测对象,自动追击玩家
public class PatrolZoneCollider : MonoBehaviour
{
/* ... */
void OnTriggerEnter(Collider collider)
{
if (collider.gameObject.tag == "Player")
{
this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = true;
this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
}
}
}
- PatrolFollowAction.cs: 实现巡逻兵追击玩家的动作。当失去玩家目标后,继续巡逻
public class PatrolFollowAction : SSAction {
private float speed = 2f;
private GameObject player;
private PatrolData data;
public override void Update() {
/* ... */
//若不存在玩家目标或玩家目标不在巡逻兵感知范围内
if (!data.follow_player || data.wall_sign != data.sign)
{
this.destroy = true;
this.callback.SSActionEvent(this, 1, this.gameobject);
}
}
/* ... */
}
使用素材包 Toony Tiny People,内有多种人物预制动画可选,根据游戏需要选用 idle、run 和 shoot。
为 Patrol
和 Player
制作动画状态机:
对于巡逻兵而言,当感知到玩家时条件 run
为真,巡逻兵从闲散的巡逻状态过渡到奔跑状态,感知消失则回到 Idle
状态;当与玩家碰撞时 shoot
事件被触发,巡逻兵向玩家开枪,游戏结束。
对于玩家而言,当接收到键盘方向键输入时条件 run
为真,玩家从闲散状态过渡到奔跑运动状态,无输入时则回到 Idle
状态;当与巡逻兵碰撞时 death
事件被触发,玩家倒地不能移动,游戏结束。