智能巡逻兵游戏设计
游戏设计要求
-
创建一个地图和若干巡逻兵。
-
每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算。
-
巡逻兵碰撞到障碍物如树,则会自动选下一个点为目标。
-
巡逻兵在设定范围内感知到玩家,会自动追击玩家。
-
失去玩家目标后,继续巡逻。
-
计分:每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束。
-
必须使用订阅与发布模式传消息、工厂模式生产巡逻兵。
游戏效果图
事件机制
与巡逻兵有关的事件有 5 个:
-
巡逻兵与玩家碰撞
-
巡逻兵与障碍物碰撞
-
巡逻兵感知到玩家
-
巡逻兵被玩家甩掉
-
巡逻兵到达目标位置
这些事件由 PatrolEventManager 统一管理,并通过 C# 的事件机制实现发布/订阅模式。
using UnityEngine;
public class PatrolEventManager {
// singleton instance
private static PatrolEventManager instance;
// hit player event
public delegate void HitPlayerAction(GameObject patrol);
public static event HitPlayerAction OnHitPlayer;
// hit obstacle event
public delegate void HitObstacleAction(GameObject patrol);
public static event HitObstacleAction OnHitObstacle;
// see player event
public delegate void SeePlayerAction(GameObject patrol);
public static event SeePlayerAction OnSeePlayer;
// lose player event
public delegate void LosePlayerAction(GameObject patrol);
public static event LosePlayerAction OnLosePlayer;
// stop event
public delegate void StopAction(GameObject patrol);
public static event StopAction OnStop;
public static PatrolEventManager GetInstance() {
if (instance == null) {
instance = new PatrolEventManager();
}
return instance;
}
public void HitPlayer(GameObject patrol) {
OnHitPlayer?.Invoke(patrol);
}
public void HitObstacle(GameObject patrol) {
OnHitObstacle?.Invoke(patrol);
}
public void SeePlayer(GameObject patrol) {
OnSeePlayer?.Invoke(patrol);
}
public void LosePlayer(GameObject patrol) {
OnLosePlayer?.Invoke(patrol);
}
public void Stop(GameObject patrol) {
OnStop?.Invoke(patrol);
}
}
为了触发上述的事件,需要为巡逻兵对象添加一个 Collider,用作 Trigger。当玩家进入 Trigger 范围时,调用 PatrolEventManager 中的 SeePlayer 方法,触发相关的回调函数(需要在后期添加,即订阅);同样的,当玩家离开 Trigger 范围时,调用 PatrolEventManager 中的 LosePlayer 方法,触发相关的回调函数。
using UnityEngine;
public class PatrolTrigger : MonoBehaviour {
private void OnTriggerEnter(Collider other) {
if (other.gameObject.tag == "Player") {
PatrolEventManager.GetInstance().SeePlayer(gameObject);
}
}
private void OnTriggerExit(Collider other) {
if (other.gameObject.tag == "Player") {
PatrolEventManager.GetInstance().LosePlayer(gameObject);
}
}
}
另外,当巡逻兵与玩家或障碍物碰撞时,依靠 PatrolEventManager 分别调用不同的回调函数:
using UnityEngine;
public class PatrolCollide : MonoBehaviour {
void OnCollisionEnter(Collision collision) {
if (collision.gameObject.tag == "Player") {
PatrolEventManager.GetInstance().HitPlayer(gameObject);
}
else {
PatrolEventManager.GetInstance().HitObstacle(gameObject);
}
}
}
与玩家的事件只有一种:移动。该事件同样由事件管理类 PlayerEventManager 控制:
public class PlayerEventManager {
// singleton instance
private static PlayerEventManager instance;
// move event
public delegate void MoveAction(float verticalAxis, float horizontalAxis);
public static event MoveAction OnMove;
public static PlayerEventManager GetInstance() {
if (instance == null) {
instance = new PlayerEventManager();
}
return instance;
}
public void Move(float verticalAxis, float horizontalAxis) {
OnMove?.Invoke(verticalAxis, horizontalAxis);
}
}
角色的移动
巡逻兵的移动由协程控制。每次设置目标位置,协程都会被启动:当巡逻兵未到达目标位置时,MoveTo 协程通过 Transform 控制巡逻兵向目标位置移动;当巡逻兵到达目标位置时,通过 PatrolEventManager 调用 Stop 事件相关的回调函数。
using UnityEngine;
using System.Collections;
public class PatrolAction : MonoBehaviour {
private readonly float smoothing = 2f;
private Vector3 target;
public Vector3 Target {
get { return target; }
set {
target = value;
StopCoroutine("MoveTo");
StartCoroutine("MoveTo", target);
}
}
IEnumerator MoveTo(Vector3 other) {
while (Vector3.Distance(transform.position, other) > 0.05f) {
transform.position = Vector3.Lerp(transform.position, other, smoothing * Time.deltaTime);
transform.LookAt(other);
yield return null;
}
PatrolEventManager.GetInstance().Stop(gameObject);
}
}
角色的移动通过控制 Transform 进行,同时通过调用 Rotate 函数调整角度:
using UnityEngine;
public class PlayerAction : MonoBehaviour {
public float speed = 7.0f;
public float rotationSpeed = 100.0f;
public void Move(float verticalAxis, float horizontalAxis) {
float translation = verticalAxis * speed;
float rotation = horizontalAxis * rotationSpeed;
translation *= Time.deltaTime;
rotation *= Time.deltaTime;
transform.Translate(0, 0, translation);
transform.Rotate(0, rotation, 0);
}
}
在 GameGUI 类中,Update 函数接收用户的输入,并通过 PlayerEventManager 调用移动相关的回调函数,最终事件玩家的移动:
using UnityEngine;
public class GameGUI : MonoBehaviour {
private IUserAction action;
private GameResult result;
private int score;
private void Start() {
Restart();
}
private void OnGUI() {
GUIStyle messageStyle = new GUIStyle();
messageStyle.fontSize = 40;
messageStyle.fontStyle = FontStyle.Bold;
GUI.Label(new Rect(10, Screen.height / 2 - 300, 200, 100), "Score: " + score, messageStyle);
if (result != GameResult.Continuing) {
GUIStyle buttonStyle = new GUIStyle("button");
buttonStyle.fontSize = 30;
buttonStyle.fontStyle = FontStyle.Bold;
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 100, 100, 20), "Fail", messageStyle);
if (GUI.Button(new Rect(Screen.width / 2 - 70, Screen.height / 2, 140, 70), "Restart", buttonStyle)) {
action.Restart();
}
}
}
private void Update() {
if (result != GameResult.Continuing) {
return;
}
float verticalAxis = Input.GetAxis("Vertical");
float horizontalAxis = Input.GetAxis("Horizontal");
PlayerEventManager.GetInstance().Move(verticalAxis, horizontalAxis);
}
public void SetState(Judger judger) {
result = judger.result;
score = judger.score;
}
public void Restart() {
action = Director.GetInstance().currentSceneController as IUserAction;
result = GameResult.Continuing;
score = 0;
}
}
裁判类
裁判会进行积分并控制游戏结果:
public enum GameResult {
Continuing,
Lose
}
public class Judger {
public GameResult result;
public int score;
public Judger() {
Restart();
}
public void AddScore() {
score += 1;
}
public void GameOver() {
result = GameResult.Lose;
}
public void Restart() {
score = 0;
result = GameResult.Continuing;
}
}
巡逻兵的生成
巡逻兵对象由工厂类 PatrolFactory 生成。在依据初始位置生成巡逻兵对象后,需要在对象中添加上文提到的元素:
using UnityEngine;
public class PatrolFactory {
// singleton instance
private static PatrolFactory instance;
public static PatrolFactory GetInstance() {
if (instance == null) {
instance = new PatrolFactory();
}
return instance;
}
public GameObject GetPatrol(float x, float z) {
GameObject patrol = Loader.LoadObj("Prefabs/Patrol", new Vector3(x, 0f, z));
patrol.AddComponent<PatrolCollide>();
patrol.AddComponent<PatrolTrigger>();
patrol.AddComponent<PatrolAction>();
return patrol;
}
}
总控制器
前文提到的回调函数需要在总控制器 FirstController 中添加:
void OnEnable() {
// patrol events
PatrolEventManager.OnHitPlayer += PatrolHitPlayer;
PatrolEventManager.OnHitObstacle += PatrolHitObstacle;
PatrolEventManager.OnSeePlayer += PatrolSeePlayer;
PatrolEventManager.OnLosePlayer += PatrolLosePlayer;
PatrolEventManager.OnStop += PatrolStop;
// player event
PlayerEventManager.OnMove += PlayerMove;
}
void OnDisable() {
// patrol events
PatrolEventManager.OnHitPlayer -= PatrolHitPlayer;
PatrolEventManager.OnHitObstacle -= PatrolHitObstacle;
PatrolEventManager.OnSeePlayer -= PatrolSeePlayer;
PatrolEventManager.OnLosePlayer -= PatrolLosePlayer;
PatrolEventManager.OnStop -= PatrolStop;
// player event
PlayerEventManager.OnMove -= PlayerMove;
}
当巡逻兵与玩家碰撞时,游戏结束:
void PatrolHitPlayer(GameObject patrol) {
// game over
judger.GameOver();
gui.SetState(judger);
}
当巡逻兵与障碍物碰撞时,寻找下一个目标位置:
void PatrolHitObstacle(GameObject patrol) {
RandomWalk(patrol);
}
当巡逻兵感知到玩家的靠近时,将巡逻兵的目标设置为玩家:
void PatrolSeePlayer(GameObject patrol) {
PatrolAction action = patrol.GetComponent<PatrolAction>() as PatrolAction;
if (action != null) {
// follow player
action.Target = player.transform.position;
}
}
当巡逻兵被玩家甩掉时,寻找下一个目标位置,并为玩家加上一分:
void PatrolLosePlayer(GameObject patrol) {
RandomWalk(patrol);
// update player's score
judger.AddScore();
gui.SetState(judger);
}
当巡逻兵到达目标位置时,继续寻找下一个目标位置:
void PatrolStop(GameObject patrol) {
RandomWalk(patrol);
}
寻找下一个目标位置的函数 RandomWalk 通过 PatrolAction 控制巡逻兵运动:
void RandomWalk(GameObject patrol) {
PatrolAction action = patrol.GetComponent<PatrolAction>() as PatrolAction;
if (action != null) {
// find a new random position
Vector3 newPosition = patrol.transform.position + new Vector3(Random.Range(-2f, 2f), 0f, Random.Range(-2f, 2f));
action.Target = newPosition;
}
}
函数 PlayerMove 通过 PlayerAction 控制玩家移动:
void PlayerMove(float verticalAxis, float horizontalAxis) {
PlayerAction action = player.GetComponent<PlayerAction>() as PlayerAction;
action.Move(verticalAxis, horizontalAxis);
}
完整项目可见 GitHub 仓库。