智能巡逻兵游戏设计

游戏设计要求

  • 创建一个地图和若干巡逻兵。

  • 每个巡逻兵走一个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 仓库

Updated: