Unity 小游戏:智能巡逻兵
对于大多数游戏来说,人工智能是不可或缺的一部分。优秀的人工智能可以让游戏更加具有挑战性,让玩家感受到更真实的游戏体验,更重要的是让游戏更加有趣。
这次的作业是要设计一个智能巡逻兵游戏,游戏设计要求如下:
1. 准备资源
资源是在官网Asset Store下载的Survival Shooter
2. 布置场景
在下载的项目中有预制物体Environment
直接拿来当作游戏地图就好了
3. 设计过程
游戏依旧采用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
4. 编辑脚本
以下是完整代码:
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 () {
}
}