一.游戏规则与游戏要求
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
- subject:OnLostGoal
- Publisher: ?
- Subscriber: ?
- 工厂模式生产巡逻兵
- 友善提示1:生成 3~5个边的凸多边型
- 随机生成矩形
- 在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
- 5 ?
- 友善提示2:参考以前博客,给出自己新玩法
- 必须使用订阅与发布模式传消息
- 游戏规则:
- 使用WSAD或方向键上下左右移动player,进入巡逻兵的追捕后逃脱可积累一分,若与巡逻兵碰撞则游戏结束。
二.游戏UML类图以及订阅-发布模式
订阅-发布模式:订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码在这个游戏中,记分员类就充当发布者角色,其他涉及加分的事件对它进行订阅,这样每次加分事件发生统一由记分员完成加分,不需要各个类分别处理,从而减少耦合,达成优化目的。
三.游戏实现
- 首先是预制体部分,我在asset store中下载了人物模型以及地形贴图并制作了地形。
任务包含一个动作,即走的时候会使用run动画。通过Animator Controllor控制人物的动画。
- 然后就是巡逻兵Patrols的创建。游戏要求通过工厂模式生产巡逻兵
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PropFactory : MonoBehaviour
{
private GameObject patrol = null;
private List<GameObject> used = new List<GameObject>();
private Vector3[] pos = new Vector3[9];
public FirstSceneController sceneControler;
public List<GameObject> GetPatrols()
{
int[] pos_x = { -6, 4, 13 };
int[] pos_z = { -4, 6, -13 };
int index = 0;
for(int i=0;i < 3;i++)
{
for(int j=0;j < 3;j++)
{
pos[index] = new Vector3(pos_x[i], 5, pos_z[j]);
index++;
}
}
for(int i=0; i < 9; i++)
{
patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol"));
patrol.transform.position = pos[i];
patrol.GetComponent<PatrolData>().sign = i + 1;
patrol.GetComponent<PatrolData>().start_position = pos[i];
used.Add(patrol);
}
return used;
}
}
这与之前的工厂模式类似。
- 然后是动作管理器,这部分与之前的类似,包含巡逻兵巡逻的动作以及巡逻兵追踪玩家的动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameobject;
public Transform transform;
public ISSActionCallback callback;
protected SSAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
public class SSActionManager : MonoBehaviour, ISSActionCallback
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();
protected void Update()
{
foreach (SSAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable)
{
ac.Update();
}
}
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
waitingDelete.Clear();
}
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
{
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
public void SSActionEvent(SSAction source, int intParam = 0, GameObject objectParam = null)
{
if(intParam == 0)
{
PatrolFollowAction follow = PatrolFollowAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().player);
this.RunAction(objectParam, follow, this);
}
else
{
GoPatrolAction move = GoPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().start_position);
this.RunAction(objectParam, move, this);
Singleton<GameEventManager>.Instance.PlayerEscape();
}
}
public void DestroyAll()
{
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
ac.destroy = true;
}
}
}
public class PatrolActionManager : SSActionManager
{
private GoPatrolAction go_patrol;
public void GoPatrol(GameObject patrol)
{
go_patrol = GoPatrolAction.GetSSAction(patrol.transform.position);
this.RunAction(patrol, go_patrol, this);
}
public void DestroyAllAction(){DestroyAll();}
}
public class GoPatrolAction : SSAction
{
private enum Dirction { E, N, W, S };
private float pos_x, pos_z;
private float move_length;
private float move_speed = 1.2f;
private bool move_sign = true;
private Dirction dirction = Dirction.E;
private PatrolData data;
private GoPatrolAction() { }
public static GoPatrolAction GetSSAction(Vector3 location)
{
GoPatrolAction action = CreateInstance<GoPatrolAction>();
action.pos_x = location.x;
action.pos_z = location.z;
action.move_length = Random.Range(5, 8);
return action;
}
public override void Update()
{
if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
if (transform.position.y != 0)transform.position = new Vector3(transform.position.x, 0, transform.position.z);
Gopatrol();
if (data.follow_player && data.wall_sign == data.sign)
{
this.destroy = true;
this.callback.SSActionEvent(this,0,this.gameobject);
}
}
public override void Start(){data = this.gameobject.GetComponent<PatrolData>();}
void Gopatrol()
{
if (move_sign)
{
switch (dirction)
{
case Dirction.E:
pos_x -= move_length;
break;
case Dirction.N:
pos_z += move_length;
break;
case Dirction.W:
pos_x += move_length;
break;
case Dirction.S:
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.S)dirction = Dirction.E;
move_sign = true;
}
}
}
public class PatrolFollowAction : SSAction
{
private float speed = 2f;
private GameObject player;
private PatrolData data;
private PatrolFollowAction() { }
public static PatrolFollowAction GetSSAction(GameObject player)
{
PatrolFollowAction action = CreateInstance<PatrolFollowAction>();
action.player = player;
return action;
}
public override void Update()
{
if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
if (transform.position.y != 0)transform.position = new Vector3(transform.position.x, 0, transform.position.z);
Follow();
if (!data.follow_player || data.wall_sign != data.sign)
{
this.destroy = true;
this.callback.SSActionEvent(this,1,this.gameobject);
}
}
public override void Start(){data = this.gameobject.GetComponent<PatrolData>();}
void Follow()
{
transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime);
this.transform.LookAt(player.transform.position);
}
}
巡逻兵巡逻的动作是GoPatrolAction函数,函数中实现了巡逻兵在不同的方向时走任意的四边形。追踪的动作是PatrolFollowAction函数,巡逻兵朝着玩家的位置移动,移动结束的条件是玩家已经不在该区域内了。巡逻动作结束条件是需要追捕玩家,所以在SSActionManager中调用了回调函数,用回调函数来进行追捕动作。而当玩家离开追捕范围后,需要重新巡逻,也需要调用回调函数,从初始的位置和方向继续巡逻。除此之外,SSActionManager还实现了游戏结束后,摧毁所有动作,巡逻兵不再移动。初始的时候场景控制器调用PatrolActionManager中的方法,让巡逻兵进行运动。当游戏结束的时候,调用方法让巡逻兵停止巡逻。
- 接着是GUI部分。 这其中实现了实时获取键盘的移动位置然后调用场景控制其之中的玩家移动函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour {
private IUserAction action;
private GUIStyle style1 = new GUIStyle();
private GUIStyle over_style = new GUIStyle();
void Start ()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
style1.normal.textColor = new Color(1, 1, 1, 1);
style1.fontSize = 16;
over_style.fontSize = 25;
}
void Update()
{
float translationX = Input.GetAxis("Horizontal");
float translationZ = Input.GetAxis("Vertical");
action.MovePlayer(translationX, translationZ);
}
private void OnGUI()
{
GUI.Label(new Rect(10, 5, 200, 50), "分数:", style1);
GUI.Label(new Rect(55, 5, 200, 50), action.GetScore().ToString(), style1);
if(action.GetGameover())
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "重新开始"))
{
action.Restart();
return;
}
}
}
- 场景控制器中实现了玩家的移动以及资源的加载。 玩家的移动当中需要实现两个操作,一个是让玩家平滑地进行移动,然后就是让玩家朝着移动的方向看。还有需要在update中每一帧检测玩家进入的块的编号,然后找到这个块中对应的巡逻兵去调用follow函数进行追踪。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController
{
public PropFactory patrol_factory;
public ScoreRecorder recorder;
public PatrolActionManager action_manager;
public int wall_sign = 4;
public GameObject player;
public float player_speed = 30;
public float rotate_speed = 10;
private List<GameObject> patrols;
private bool game_over = false;
public int GetScore(){return recorder.GetScore();}
public bool GetGameover(){return game_over;}
public void Restart(){SceneManager.LoadScene(0);}
void OnEnable(){GameEventManager.ScoreChange += AddScore;GameEventManager.GameoverChange += Gameover;}
void OnDisable(){GameEventManager.ScoreChange -= AddScore;GameEventManager.GameoverChange -= Gameover;}
void AddScore(){recorder.AddScore();}
void Gameover(){game_over = true;action_manager.DestroyAllAction();}
void Update()
{
for (int i = 0; i < patrols.Count; i++)patrols[i].gameObject.GetComponent<PatrolData>().wall_sign = wall_sign;
for (int i = 0; i < patrols.Count; i++)
if (patrols [i].gameObject.GetComponent<PatrolData> ().sign == patrols [i].gameObject.GetComponent<PatrolData> ().wall_sign) {
patrols [i].gameObject.GetComponent<PatrolData> ().follow_player = true;
patrols [i].gameObject.GetComponent<PatrolData> ().player = player;
} else {
patrols [i].gameObject.GetComponent<PatrolData>().follow_player = false;
patrols [i].gameObject.GetComponent<PatrolData>().player = null;
}
}
void Start()
{
wall_sign = 4;
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
patrol_factory = Singleton<PropFactory>.Instance;
action_manager = gameObject.AddComponent<PatrolActionManager>() as PatrolActionManager;
LoadResources();
recorder = Singleton<ScoreRecorder>.Instance;
}
public void LoadResources()
{
Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));
player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(0, 9, 0), Quaternion.identity) as GameObject;
patrols = patrol_factory.GetPatrols();
for (int i = 0; i < patrols.Count; i++) action_manager.GoPatrol(patrols[i]);
}
public Vector3 movement;
public void MovePlayer(float translationX, float translationZ)
{
if(!game_over)
{
Vector3 direction = new Vector3(translationX,0,translationZ).normalized;
player.transform.position = Vector3.MoveTowards(player.transform.position, player.transform.position+direction, player_speed * Time.deltaTime);
player.transform.LookAt(player.transform.position+direction);
}
}
}
通过调用normalized实现平滑移动,然后通过LookAt函数进行转向,与巡逻兵的运动操作类似。检测追踪的操作在update中,遍历所有巡逻兵,如果自己所属块的编号与当前玩家所在的编号相同,则进行追踪,否则进行巡逻运动。最后在OnEnable和OnDisable中实现发布订阅模式的操作。
- 接着是订阅与发布模式的部分。 该部分动作是通过订阅与发布模式实现的,首先定义一个发布事件的类GameEventManager,订阅者订阅该类中声明的事件,当其他类发生改变的时候,会使用GameEventManager的方法发布消息。场景控制器是订阅者,订阅了GameEventManager中的事件,如果在场景中相应事件发生,那么场景控制器就会调用相应的方法,即得分操作与游戏结束操作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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();
}
}
}
- 接着是碰撞部分。 有以下几种碰撞操作:玩家进入区域内的碰撞,玩家离开区域内的碰撞以及玩家碰到巡逻兵的碰撞。前两个碰撞实现在同一类中,挂载到区域的触发器上,当玩家进入或退出就会触发类中的函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AreaCollide : MonoBehaviour
{
public int sign = 0;
FirstSceneController sceneController;
private void Start(){sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;}
void OnTriggerEnter(Collider collider){if (collider.gameObject.name == "Player(Clone)")sceneController.wall_sign = sign;}
}
最后一种碰撞玩家碰到巡逻兵的碰撞实现的类挂载到巡逻兵上,若碰到则触发相应的函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCollide : MonoBehaviour{void OnCollisionEnter(Collision other){if (other.gameObject.name == "Player(Clone)")Singleton<GameEventManager>.Instance.PlayerGameover();}}
- 最后是计分器类,接口,导演类以及单例模式类,这与之前作业的类基本相同。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour
{
public FirstSceneController sceneController;
public int score = 0;
void Start() {sceneController = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;sceneController.recorder = this;}
public int GetScore(){return score;}
public void AddScore(){score++;}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController{void LoadResources();}
public interface IUserAction
{
void MovePlayer(float translationX, float translationZ);
int GetScore();
bool GetGameover();
void Restart();
}
public interface ISSActionCallback{void SSActionEvent(SSAction source,int intParam = 0,GameObject objectParam = null);}
public interface IGameStatusOp
{
void PlayerEscape();
void PlayerGameover();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object
{
private static SSDirector _instance;
public ISceneController CurrentScenceController { get; set; }
public static SSDirector GetInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T)
+ " is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
这样就实现了智能巡逻兵的游戏。
四.实验总结与心得
在这次作业中使用了订阅-发布模式,我也对这个模式有了更深的了解。订阅-发布模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
视频展示:智能巡逻兵
Github地址:Patrols
最后感谢师兄的博客!