Unity3d游戏(七): 智能巡逻兵
智能巡逻兵
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
设计过程:
采用MVC架构,用工厂模式产生巡逻兵。
巡逻兵的设计:
巡逻兵的路径为一个3~5个边的凸多边形,需因此要一些数组来存放巡逻兵路径的相对位置:
if (sideNum == 3) { posSet = new Vector3[] {new Vector3 (0, 0, 0), new Vector3 (8, 0, 0), new Vector3 (4, 0, 6), new Vector3 (0, 0, 0)}; } else if (sideNum == 4) { posSet = new Vector3[] {new Vector3 (0, 0, 0), new Vector3 (8, 0, 0), new Vector3 (8, 0, 8), new Vector3 (0, 0, 8), new Vector3 (0, 0, 0)}; } else { posSet = new Vector3[] {new Vector3 (0, 0, 0), new Vector3 (5, 0, 0), new Vector3 (7, 0, 5), new Vector3 (3, 0, 8), new Vector3 (-2, 0, 5), new Vector3 (0, 0, 0)}; }
Patrol
的Update ()
方法来实现巡逻兵的动作
void Update () { if (playerIsLive && inField (playerPos)) { //玩家存活并且在此巡逻兵视野中 catchPlayer (); //追击玩家 } else { if (patrolInMap (currentSide)) { //否则沿着自己的路径巡逻 if (++currentSide >= sideNum) { bothPos = transform.position; currentSide = 0; } } } }
其中追击玩家以及巡逻的具体实现参考后面的代码Patrol.cs
设计的重点是使用发布订阅模式完成Player
、Patrol
、SceneController
以及ScoreManager
之间的通信。首先定义了一个抽象类Subject.cs(发布者)和一个接口Handle.cs(订阅者)。
Subject
中有一个List<Handle>
存放所有的Handle,并且有增加和删除Handle的方法Attach ()
和Detach ()
。还有一个Notify ()
方法向每一个Handle
发送消息。Handle
中则有处理Subject
发送过来的消息的方法Reaction ()
。Player
继承Subject
作为发布者将自己的状态以及位置发送给每一个Handle
public override void Notify (bool live, Vector3 pos) { foreach (Handle h in handles) { h.Reaction(live, pos); } }
Patrol
、
SceneController和
ScoreManager都作为
Handle
接受到
Player
发送过来的消息,但是不同的
Handle
则会对这些信息做出不同地处理。巡逻兵
Patrol
需要知道玩家的位置,以及是否存活;而
SceneController
和
ScoreManager
并不在乎玩家的位置,它们只关心玩家是不是还活着。
至于ScoreManager
中的计分操作,是用事件(Event)与委托(delegate)的观察者模式实现。Patrol
中声明了委托getScore
与事件escape
,以及注册、注销观察者的方法。
public delegate void getScore (int n); public event getScore escape; public void register (getScore s) { escape += s; } public void unRegister (getScore s) { escape -= s; }
在每个巡逻兵被创建时,注册ScoreManager
为观察者,当玩家离开巡逻兵的视野,巡逻兵发出escape
的信号,然后ScoreManager
就执行计分操作。
游戏的难度也不是一成不变的,每当玩家分数增加到一定数量,又会产生新的,更强的巡逻兵!具体实现见SceneController.cs
编译脚本
以下为完整代码:
SSDirector.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SSDirector : System.Object { //singleton instance private static SSDirector instance; public ISceneController currentScene; public bool running { get; set; } public static SSDirector getInstance () { if (instance == null) { instance = new SSDirector (); } return instance; } }
ISceneController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface ISceneController { void LoadResources (); void CreatePatrols (); void CreateMore (); }
SceneController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using com.myspace; public class SceneController : MonoBehaviour, ISceneController, IUserAction, IScore, Handle { public GameObject player; private SSDirector director; private bool canOperation; private bool create; private int bearNum; private int ellephantNum; private Subject sub; private Animator ani; private Vector3 movement; // The vector to store the direction of the player's movement. void Awake () { director = SSDirector.getInstance (); sub = player.GetComponent<Player> (); ani = player.GetComponent<Animator> (); director.currentScene = this; director.currentScene.LoadResources (); director.currentScene.CreatePatrols (); Handle sc = director.currentScene as Handle; sub.Attach (sc); GetComponent<ScoreManager> ().resetScore (); bearNum = 0; ellephantNum = 0; create = false; } void Update () { int score = GetComponent<ScoreManager> ().getScore (); if (score % 10 == 0) { director.currentScene.CreateMore (); } else { create = true; } } #region ISceneController public void LoadResources () { GameObject Environment = Instantiate<GameObject> ( Resources.Load<GameObject> ("Prefabs/Environment")); Environment.name = "Environment"; } public void CreatePatrols () { //创建游戏开始时的巡逻兵 PatrolFactory pf = PatrolFactory.getInstance (); for (int i = 1; i <= 12; i++) { GameObject patrol = pf.getPatrol (); patrol.name = "Patrol" + ++bearNum; Handle p = patrol.GetComponent<Patrol> (); sub.Attach (p); patrol.GetComponent<Patrol> ().register (GetComponent<ScoreManager> ().addScore); } } public void CreateMore () { //每增加十分,创建新的巡逻兵 if (create) { PatrolFactory pf = PatrolFactory.getInstance (); for (int i = 1; i <= 3; i++) { GameObject patrol = pf.getPatrol (); patrol.name = "Patrol" + ++ellephantNum; Handle p = patrol.GetComponent<Patrol> (); sub.Attach (p); patrol.GetComponent<Patrol> ().register (GetComponent<ScoreManager> ().addScore); } for (int i = 1; i <= 3; i++) { GameObject patrolplus = pf.getPatrolPlus (); patrolplus.name = "Patrolplus" + ++bearNum; Handle p = patrolplus.GetComponent<Patrol> (); sub.Attach (p); patrolplus.GetComponent<Patrol> ().register (GetComponent<ScoreManager> ().addScore); } create = false; } } #endregion #region IUserAction public void movePlayer (float h, float v) { if (canOperation) { player.GetComponent<Player> ().move (h, v); if (h == 0 && v == 0) { ani.SetTrigger ("stop"); } else { ani.SetTrigger ("move"); } } } public void setDirection (float h, float v) { if (canOperation) { player.GetComponent<Player> ().turn (h, v); } } public bool GameOver () { return (!canOperation); } #endregion #region ISceneController public int currentScore () { return GetComponent<ScoreManager> ().getScore (); } #endregion #region Handele public void Reaction (bool isLive, Vector3 pos) { ani.SetBool ("live", isLive); canOperation = isLive; } #endregion }
IUserAction.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface IUserAction { void movePlayer (float h, float v); void setDirection (float h, float v); bool GameOver (); }
UI.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; public class UI : MonoBehaviour { private IUserAction action; private IScore score; public Transform player; // The position that that camera will be following. public float smoothing = 5f; // The speed with which the camera will be following. public Text s; public Text gg; public Button re; Vector3 offset; // The initial offset from the player. // Use this for initialization void Start () { action = SSDirector.getInstance ().currentScene as IUserAction; score = SSDirector.getInstance ().currentScene as IScore; // Calculate the initial offset. offset = transform.position - player.position; re.gameObject.SetActive (false); Button btn = re.GetComponent<Button> (); btn.onClick.AddListener(restart); } void Update () { // Create a postion the camera is aiming for based on the offset from the player. Vector3 playerCamPos = player.position + offset; // Smoothly interpolate between the camera's current position and it's player position. transform.position = Vector3.Lerp (transform.position, playerCamPos, smoothing * Time.deltaTime); float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); move (h, v); turn (h, v); showScore (); gameOver (); } //移动玩家 public void move (float h, float v) { action.movePlayer (h, v); } //使玩家面向移动方向 public void turn (float h, float v) { if (h != 0 || v != 0) { action.setDirection (h, v); } } //显示分数 public void showScore () { s.text = "Score : " + score.currentScore (); } //游戏结束 public void gameOver () { if (action.GameOver ()) { if (!re.isActiveAndEnabled) { re.gameObject.SetActive (true); } gg.text = "Game Over!"; } } //重新开始 public void restart () { SceneManager.LoadScene ("main"); } }
Subject.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class Subject : MonoBehaviour { protected List<Handle> listeners = new List<Handle> (); public abstract void Attach (Handle listener); public abstract void Detach (Handle listener); public abstract void Notify (bool live, Vector3 pos); }
Handle.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface Handle { void Reaction (bool isLive, Vector3 pos); }
Player.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : Subject { private bool isLive; private Vector3 position; private float speed; Vector3 movement; // The vector to store the direction of the player's movement. protected List<Handle> handles = new List<Handle> (); //所有观察者 // Use this for initialization void Start () { isLive = true; speed = 8.0f; } public override void Attach (Handle h) { handles.Add (h); } public override void Detach (Handle h) { handles.Remove (h); } public override void Notify (bool live, Vector3 pos) { foreach (Handle h in handles) { h.Reaction(live, pos); } } //玩家碰到巡逻兵,就死亡 void OnCollisionEnter (Collision other) { if (other.gameObject.tag == "patrol") { isLive = false; } } // Update is called once per frame void Update () { position = transform.position; Notify (isLive, position); } public void move (float h, float v) { if (isLive) { // Set the movement vector based on the axis input. movement.Set (h, 0f, v); // Normalise the movement vector and make it proportional to the speed per second. movement = movement.normalized * speed * Time.deltaTime; // Move the player to it's current position plus the movement. GetComponent<Rigidbody> ().MovePosition (transform.position + movement); } } public void turn (float h, float v) { if (isLive) { // Set the movement vector based on the axis input. movement.Set (h, 0f, v); Quaternion rot = Quaternion.LookRotation (movement); // Set the player's rotation to this new rotation. GetComponent<Rigidbody> ().rotation = rot; } } }
Patrol.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Patrol : MonoBehaviour, Handle { protected Vector3 bothPos; private bool playerIsLive; private Vector3 playerPos; private Vector3[] posSet; private int currentSide; private int sideNum; private bool turn; private bool isCatching = false; public int score = 1; public float field = 7f; public float speed = 1f; public delegate void getScore (int n); public event getScore escape; public void register (getScore s) { escape += s; } public void unRegister (getScore s) { escape -= s; } // Use this for initialization void Start () { transform.position = getBothPos (); bothPos = transform.position; } void Awake () { turn = false; sideNum = Random.Range (3, 6); currentSide = 0; if (sideNum == 3) { posSet = new Vector3[] { new Vector3 (0, 0, 0), new Vector3 (8, 0, 0), new Vector3 (4, 0, 6), new Vector3 (0, 0, 0) }; } else if (sideNum == 4) { posSet = new Vector3[] { new Vector3 (0, 0, 0), new Vector3 (8, 0, 0), new Vector3 (8, 0, 8), new Vector3 (0, 0, 8), new Vector3 (0, 0, 0) }; } else { posSet = new Vector3[] { new Vector3 (0, 0, 0), new Vector3 (5, 0, 0), new Vector3 (7, 0, 5), new Vector3 (3, 0, 8), new Vector3 (-2, 0, 5), new Vector3 (0, 0, 0) }; } } void OnCollisionEnter (Collision other) { turn = true; } public bool inField (Vector3 targetPos) { float distance = (transform.position - targetPos).sqrMagnitude; if (distance <= field * field) { return true; } return false; } public void Reaction (bool isLive, Vector3 pos) { playerIsLive = isLive; playerPos = pos; } public void catchPlayer () { bothPos = transform.position; isCatching = true; transform.LookAt (playerPos); transform.position = Vector3.Lerp (transform.position, playerPos, speed * Time.deltaTime); } public bool patrolInMap (int side) { if (isCatching && playerIsLive) { isCatching = false; if (escape != null) { escape (score); } } if (turn) { turn = false; Vector3 v = transform.forward; Quaternion dir = Quaternion.LookRotation (v); Quaternion toDir = Quaternion.LookRotation (-v); transform.rotation = Quaternion.RotateTowards (dir, toDir, 1f); return true; } if (transform.position != bothPos + posSet [side + 1]) { transform.LookAt (bothPos + posSet [side + 1]); transform.position = Vector3.Lerp (transform.position , bothPos + posSet [side + 1], speed * Time.deltaTime); } if ((transform.position - (bothPos + posSet [side + 1])).sqrMagnitude <= 0.1f) { return true; } return false; } public Vector3 getBothPos () { while (true) { Vector3 pos = new Vector3 (Random.Range (-30f, 30f), 0, Random.Range (-30f, 30f)); if ((pos - Vector3.zero).sqrMagnitude >= 100f) { return pos; } } } // Update is called once per frame void Update () { if (playerIsLive && inField (playerPos)) { catchPlayer (); } else { if (patrolInMap (currentSide)) { if (++currentSide >= sideNum) { bothPos = transform.position; currentSide = 0; } } } } }
PatrolFactory.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace com.myspace{ public class PatrolFactory : System.Object { private static PatrolFactory instance; public static PatrolFactory getInstance () { if (instance == null) { instance = new PatrolFactory (); } return instance; } public GameObject getPatrol () { GameObject patrol = GameObject.Instantiate<GameObject> ( Resources.Load<GameObject> ("Prefabs/Patrol"));; return patrol; } public GameObject getPatrolPlus () { GameObject patrolplus = GameObject.Instantiate<GameObject> ( Resources.Load<GameObject> ("Prefabs/Patrolplus"));; return patrolplus; } public void freePatrol (GameObject p) { p.SetActive (false); } } }
IScore.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface IScore { int currentScore (); }
ScoreManager.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ScoreManager : MonoBehaviour, Handle { private int score; private bool playerIsLive; public void Reaction (bool isLive, Vector3 pos) { playerIsLive = isLive; } public int getScore () { return score; } public void addScore (int s) { if (playerIsLive) { score += s; } } public void resetScore () { score = 0; } void Awake () { playerIsLive = true; score = 0; } void Update () { } }