演示视频链接 优酷视频链接
Github项目文件传送门 Github项目文件传送门
- 设计要求
- 游戏设计要求
- 创建一个地图和若干巡逻兵;
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物如墙体,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束
- 程序设计要求
- 必须使用订阅与发布模式传消息、工厂模式生产巡逻兵
- 游戏设计要求
- 类图(因为参考的博客是老师推荐的结构,所以没有轻易改动主要结构)
- 详细代码分析
这个脚本是用来监测用户的键盘输入控制里面玩家角色的移动,可以上下左右移动
UserIntetrface.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
public class UserInterface : MonoBehaviour {
private IUserAction action;
void Start () {
action = SceneController.getInstance() as IUserAction;
}
void Update () {
detectKeyInput();
}
void detectKeyInput() {
if (Input.GetKey(KeyCode.UpArrow)) {
action.heroMove(Diretion.UP);
}
if (Input.GetKey(KeyCode.DownArrow)) {
action.heroMove(Diretion.DOWN);
}
if (Input.GetKey(KeyCode.LeftArrow)) {
action.heroMove(Diretion.LEFT);
}
if (Input.GetKey(KeyCode.RightArrow)) {
action.heroMove(Diretion.RIGHT);
}
}
}
这个脚本文件主要是处理游戏中出现的一些情况的处理,但是具体实现在GameModel里面,这只是起一个场景控制的作用
SceneController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Com.Patrols {
public class Diretion {
public const int UP = 0;
public const int DOWN = 2;
public const int LEFT = -1;
public const int RIGHT = 1;
}
public class FenchLocation {
public const float FenchHori = 12.42f;
public const float FenchVertLeft = -3.0f;
public const float FenchVertRight = 3.0f;
}
public interface IUserAction {
void heroMove(int dir);
}
public interface IAddAction {
void addRandomMovement(GameObject sourceObj, bool isActive);
void addDirectMovement(GameObject sourceObj);
}
public interface IGameStatusOp {
int getHeroStandOnArea();
void heroEscapeAndScore();
void patrolHitHeroAndGameover();
}
public class SceneController : System.Object, IUserAction, IAddAction, IGameStatusOp {
private static SceneController instance;
private GameModel myGameModel;
private GameEventManager myGameEventManager;
public static SceneController getInstance() {
if (instance == null)
instance = new SceneController();
return instance;
}
internal void setGameModel(GameModel _myGameModel) {
if (myGameModel == null) {
myGameModel = _myGameModel;
}
}
internal void setGameEventManager(GameEventManager _myGameEventManager) {
if (myGameEventManager == null) {
myGameEventManager = _myGameEventManager;
}
}
public void heroMove(int dir) {
myGameModel.heroMove(dir);
}
public void addRandomMovement(GameObject sourceObj, bool isActive) {
myGameModel.addRandomMovement(sourceObj, isActive);
}
public void addDirectMovement(GameObject sourceObj) {
myGameModel.addDirectMovement(sourceObj);
}
public int getHeroStandOnArea() {
return myGameModel.getHeroStandOnArea();
}
public void heroEscapeAndScore() {
myGameEventManager.heroEscapeAndScore();
}
public void patrolHitHeroAndGameover() {
myGameEventManager.patrolHitHeroAndGameover();
}
}
}
类似于打飞碟游戏,这个脚本文件用来产生复制品的巡逻兵,以及确定他们可以走的方向集合
PatrolFactory.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
namespace Com.Patrols {
public class PatrolFactory : System.Object {
private static PatrolFactory instance;
private GameObject PatrolItem;
private Vector3[] PatrolPosSet = new Vector3[] { new Vector3(-6, 0, 16), new Vector3(-1, 0, 19),
new Vector3(6, 0, 16), new Vector3(-5, 0, 7), new Vector3(0, 0, 7), new Vector3(6, 0, 7)};
public static PatrolFactory getInstance() {
if (instance == null)
instance = new PatrolFactory();
return instance;
}
public void initItem(GameObject _PatrolItem) {
PatrolItem = _PatrolItem;
}
public GameObject getPatrol() {
GameObject newPatrol = Camera.Instantiate(PatrolItem);
return newPatrol;
}
public Vector3[] getPosSet() {
return PatrolPosSet;
}
}
}
这个就是具体的巡逻兵移动的脚本代码,每帧判断是否碰到玩家人物,碰到的话判断游戏结束,没有的话如果碰到墙体等障碍物就会改变方向
PatrolBehaviour.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
public class PatrolBehaviour : MonoBehaviour {
private IAddAction addAction;
private IGameStatusOp gameStatusOp;
public int ownIndex;
public bool isCatching;
private float CATCH_RADIUS = 3.0f;
void Start () {
addAction = SceneController.getInstance() as IAddAction;
gameStatusOp = SceneController.getInstance() as IGameStatusOp;
ownIndex = getOwnIndex();
isCatching = false;
}
void Update () {
checkNearByHero();
}
int getOwnIndex() {
string name = this.gameObject.name;
char cindex = name[name.Length - 1];
int result = cindex - '0';
return result;
}
void checkNearByHero () {
if (gameStatusOp.getHeroStandOnArea() == ownIndex) {
if (!isCatching) {
isCatching = true;
addAction.addDirectMovement(this.gameObject);
}
}
else {
if (isCatching) {
gameStatusOp.heroEscapeAndScore();
isCatching = false;
addAction.addRandomMovement(this.gameObject, false);
}
}
}
void OnCollisionStay(Collision e) {
if (e.gameObject.name.Contains("Patrol") || e.gameObject.name.Contains("fence")
|| e.gameObject.tag.Contains("FenceAround")) {
isCatching = false;
addAction.addRandomMovement(this.gameObject, false);
}
if (e.gameObject.name.Contains("Hero")) {
gameStatusOp.patrolHitHeroAndGameover();
}
}
}
这个是玩家人物身上的脚本,人物身上有个所在区域的变量信息,每个巡逻兵时刻检测该信息,来判断是否在自己区域,从而有对应行动
HeroStatus.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
public class HeroStatus : MonoBehaviour {
public int standOnArea = -1;
void Start () {
}
void Update () {
modifyStandOnArea();
}
void modifyStandOnArea() {
float posX = this.gameObject.transform.position.x;
float posZ = this.gameObject.transform.position.z;
if (posZ >= FenchLocation.FenchHori) {
if (posX < FenchLocation.FenchVertLeft)
standOnArea = 0;
else if (posX > FenchLocation.FenchVertRight)
standOnArea = 2;
else
standOnArea = 1;
}
else {
if (posX < FenchLocation.FenchVertLeft)
standOnArea = 3;
else if (posX > FenchLocation.FenchVertRight)
standOnArea = 5;
else
standOnArea = 4;
}
}
}
这个就是一些界面上的提示信息,预制好一个prefab,代表分数的面板和倒计时的面板和结束游戏的面板,起不同的名字,通过name区分并根据游戏进展做调整
GameStatusText.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameStatusText : MonoBehaviour {
private int score = 0;
private int textType;
private bool isEnd = false;
private Timer _timer;
void Start () {
_timer = new Timer(30);
_timer.tickEvent += SendMessage;
_timer.StartTimer();
distinguishText();
}
void Update () {
_timer.UpdateTimer(Time.deltaTime);
if (_timer.DisplayTime() <= 0)
gameOver();
if (textType == 2 && !isEnd){
gameObject.GetComponent<Text>().text = "剩余时间:" + (_timer.DisplayTime() >= 0 ? _timer.DisplayTime() : 0) + " 秒";
}
}
private void SendMessage()
{
Debug.Log("Game Over!");
}
void distinguishText() {
if (gameObject.name.Contains("Score"))
textType = 0;
else if (gameObject.name.Contains("GameOver"))
textType = 1;
else
textType = 2;
}
void OnEnable() {
GameEventManager.myGameScoreAction += gameScore;
GameEventManager.myGameOverAction += gameOver;
}
void OnDisable() {
GameEventManager.myGameScoreAction -= gameScore;
GameEventManager.myGameOverAction -= gameOver;
}
void gameScore() {
if(!isEnd)
score++;
if (!isEnd && textType == 0) {
this.gameObject.GetComponent<Text>().text = "得分: " + score;
}
}
void gameOver() {
isEnd = true;
_timer.StopTimer();
if (textType == 1)
{
this.gameObject.GetComponent<Text>().text = "游戏结束!\n您的总得分为:" + score;
}
if(textType == 0)
{
this.gameObject.GetComponent<Text>().text = "游戏已结束! 将不再计分";
}
if(textType == 2)
{
this.gameObject.GetComponent<Text>().text = "游戏时间:" + _timer.GetTime() + " 秒";
}
}
}
这个就类似于导演的控制,大概作用就是实现巡逻兵的循环走动,并且有随机的方向(如果有玩家出现在一定范围内方向就是对着玩家的反向),还有判断是否走出界
GameModel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
public class GameModel : SSActionManager, ISSActionCallback {
public GameObject PatrolItem, HeroItem, sceneModelItem, canvasItem;
private SceneController scene;
private GameObject myHero, sceneModel, canvasAndText;
private List<GameObject> PatrolSet;
private List<int> PatrolLastDir;
private const float PERSON_SPEED_NORMAL = 0.05f;
private const float PERSON_SPEED_CATCHING = 0.06f;
void Awake() {
PatrolFactory.getInstance().initItem(PatrolItem);
}
protected new void Start () {
scene = SceneController.getInstance();
scene.setGameModel(this);
genHero();
genPatrols();
sceneModel = Instantiate(sceneModelItem);
canvasAndText = Instantiate(canvasItem);
}
protected new void Update() {
base.Update();
}
void genHero() {
myHero = Instantiate(HeroItem);
}
void genPatrols() {
PatrolSet = new List<GameObject>(6);
PatrolLastDir = new List<int>(6);
Vector3[] posSet = PatrolFactory.getInstance().getPosSet();
for (int i = 0; i < 6; i++) {
GameObject newPatrol = PatrolFactory.getInstance().getPatrol();
newPatrol.transform.position = posSet[i];
newPatrol.name = "Patrol" + i;
PatrolLastDir.Add(-2);
PatrolSet.Add(newPatrol);
addRandomMovement(newPatrol, true);
}
}
public void heroMove(int dir) {
myHero.transform.rotation = Quaternion.Euler(new Vector3(0, dir * 90, 0));
switch (dir) {
case Diretion.UP:
myHero.transform.position += new Vector3(0, 0, 0.1f);
break;
case Diretion.DOWN:
myHero.transform.position += new Vector3(0, 0, -0.1f);
break;
case Diretion.LEFT:
myHero.transform.position += new Vector3(-0.1f, 0, 0);
break;
case Diretion.RIGHT:
myHero.transform.position += new Vector3(0.1f, 0, 0);
break;
}
}
public void SSActionEvent(SSAction source, SSActionEventType eventType = SSActionEventType.Completed,
SSActionTargetType intParam = SSActionTargetType.Normal, string strParam = null, object objParam = null) {
if (intParam == SSActionTargetType.Normal)
addRandomMovement(source.gameObject, true);
else
addDirectMovement(source.gameObject);
}
public void addRandomMovement(GameObject sourceObj, bool isActive) {
int index = getIndexOfObj(sourceObj);
int randomDir = getRandomDirection(index, isActive);
PatrolLastDir[index] = randomDir;
sourceObj.transform.rotation = Quaternion.Euler(new Vector3(0, randomDir * 90, 0));
Vector3 target = sourceObj.transform.position;
switch (randomDir) {
case Diretion.UP:
target += new Vector3(0, 0, 1);
break;
case Diretion.DOWN:
target += new Vector3(0, 0, -1);
break;
case Diretion.LEFT:
target += new Vector3(-1, 0, 0);
break;
case Diretion.RIGHT:
target += new Vector3(1, 0, 0);
break;
}
addSingleMoving(sourceObj, target, PERSON_SPEED_NORMAL, false);
}
int getIndexOfObj(GameObject sourceObj) {
string name = sourceObj.name;
char cindex = name[name.Length - 1];
int result = cindex - '0';
return result;
}
int getRandomDirection(int index, bool isActive) {
int randomDir = Random.Range(-1, 3);
if (!isActive) {
while (PatrolLastDir[index] == randomDir || PatrolOutOfArea(index, randomDir)) {
randomDir = Random.Range(-1, 3);
}
}
else {
while (PatrolLastDir[index] == 0 && randomDir == 2
|| PatrolLastDir[index] == 2 && randomDir == 0
|| PatrolLastDir[index] == 1 && randomDir == -1
|| PatrolLastDir[index] == -1 && randomDir == 1
|| PatrolOutOfArea(index, randomDir)) {
randomDir = Random.Range(-1, 3);
}
}
return randomDir;
}
bool PatrolOutOfArea(int index, int randomDir) {
Vector3 patrolPos = PatrolSet[index].transform.position;
float posX = patrolPos.x;
float posZ = patrolPos.z;
switch (index) {
case 0:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
return true;
break;
case 1:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
|| randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
return true;
break;
case 2:
if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
return true;
break;
case 3:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
return true;
break;
case 4:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
|| randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
return true;
break;
case 5:
if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
return true;
break;
}
return false;
}
public void addDirectMovement(GameObject sourceObj) {
int index = getIndexOfObj(sourceObj);
PatrolLastDir[index] = -2;
sourceObj.transform.LookAt(sourceObj.transform);
Vector3 oriTarget = myHero.transform.position - sourceObj.transform.position;
Vector3 target = new Vector3(oriTarget.x / 4.0f, 0, oriTarget.z / 4.0f);
target += sourceObj.transform.position;
addSingleMoving(sourceObj, target, PERSON_SPEED_CATCHING, true);
}
void addSingleMoving(GameObject sourceObj, Vector3 target, float speed, bool isCatching) {
this.runAction(sourceObj, CCMoveToAction.CreateSSAction(target, speed, isCatching), this);
}
void addCombinedMoving(GameObject sourceObj, Vector3[] target, float[] speed, bool isCatching) {
List<SSAction> acList = new List<SSAction>();
for (int i = 0; i < target.Length; i++) {
acList.Add(CCMoveToAction.CreateSSAction(target[i], speed[i], isCatching));
}
CCSequeneActions MoveSeq = CCSequeneActions.CreateSSAction(acList);
this.runAction(sourceObj, MoveSeq, this);
}
public int getHeroStandOnArea() {
return myHero.GetComponent<HeroStatus>().standOnArea;
}
}
这个脚本主要是确定游戏状态,游戏在进行就加分数,减少时间,如果结束就弹出结束面板
GameEventManager.cd
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
public class GameEventManager : MonoBehaviour {
public delegate void GameScoreAction();
public static event GameScoreAction myGameScoreAction;
public delegate void GameOverAction();
public static event GameOverAction myGameOverAction;
private SceneController scene;
void Start () {
scene = SceneController.getInstance();
scene.setGameEventManager(this);
}
void Update () {
}
public void heroEscapeAndScore() {
if (myGameScoreAction != null)
myGameScoreAction();
}
public void patrolHitHeroAndGameover() {
if (myGameOverAction != null)
myGameOverAction();
}
}
这里加了个计算倒计时的类,默认游戏时长为30s,如果时间到了,即使没有碰到巡逻兵,也会结束游戏
Timer.cs
public class Timer
{
bool _isTicking;//是否在计时中
float _currentTime;//当前时间
float _endTime;//结束时间
public delegate void EventHander();
public event EventHander tickEvent;
public Timer(float second)
{
_currentTime = 0;
_endTime = second;
}
/// <summary>
/// 开始计时
/// </summary>
public void StartTimer()
{
_isTicking = true;
}
/// <summary>
/// 更新中
/// </summary>
public void UpdateTimer(float deltaTime)
{
if (_isTicking)
{
_currentTime += deltaTime;
if (_currentTime > _endTime)
{
_isTicking = false;
tickEvent();
}
}
}
/// <summary>
/// 停止计时
/// </summary>
public void StopTimer()
{
_isTicking = false;
}
/// <summary>
/// 持续计时
/// </summary>
public void ContinueTimer()
{
_isTicking = true;
}
/// <summary>
/// 重新计时
/// </summary>
public void ReStartTimer()
{
_isTicking = true;
_currentTime = 0;
}
/// <summary>
/// 重新设定计时器
/// </summary>
public void ResetEndTimer(float second)
{
_endTime = second;
}
public float DisplayTime()
{
return _endTime - _currentTime;
}
public float GetTime()
{
if (_currentTime <= _endTime)
return _currentTime;
else
return _endTime;
}
}
另外就是具体的动作实现的一些类
CCMoveToAction.cs
using System;
using System.Collections.Generic;
using UnityEngine;
public class CCMoveToAction: SSAction {
public Vector3 target;
public float speed;
public bool isCatching;
public static CCMoveToAction CreateSSAction(Vector3 _target, float _speed, bool _isCatching) {
CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
action.target = _target;
action.speed = _speed;
action.isCatching = _isCatching;
return action;
}
public override void Start() {
}
public override void Update() {
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed);
if (this.transform.position == target) {
this.destroy = true;
if (!isCatching)
this.callBack.SSActionEvent(this);
else
this.callBack.SSActionEvent(this, SSActionEventType.Completed, SSActionTargetType.Catching);
}
}
}
CCSequenceAction.cs
using System;
using System.Collections.Generic;
using UnityEngine;
public class CCSequeneActions : SSAction, ISSActionCallback {
public List<SSAction> actionList;
public int repeatTimes = -1;
public int subActionIndex = 0;
public static CCSequeneActions CreateSSAction(List<SSAction> _actionList, int _repeatTimes = 0) {
CCSequeneActions action = ScriptableObject.CreateInstance<CCSequeneActions>();
action.repeatTimes = _repeatTimes;
action.actionList = _actionList;
return action;
}
public override void Start() {
foreach (SSAction action in actionList) {
action.gameObject = this.gameObject;
action.transform = this.transform;
action.callBack = this;
action.Start();
}
}
public override void Update() {
if (actionList.Count == 0)
return;
else if (subActionIndex < actionList.Count) {
actionList[subActionIndex].Update();
}
}
public void SSActionEvent(SSAction source,
SSActionEventType eventType = SSActionEventType.Completed,
SSActionTargetType intParam = SSActionTargetType.Normal, string strParam = null, object objParam = null) {
source.destroy = false;
this.subActionIndex++;
if (this.subActionIndex >= actionList.Count) {
this.subActionIndex = 0;
if (repeatTimes > 0)
repeatTimes--;
if (repeatTimes == 0) {
this.destroy = true;
this.callBack.SSActionEvent(this);
}
}
}
void OnDestroy() {
}
}
ISSActionCallback.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public enum SSActionEventType: int { Started, Completed }
public enum SSActionTargetType : int { Normal, Catching }
public interface ISSActionCallback {
void SSActionEvent(SSAction source,
SSActionEventType eventType = SSActionEventType.Completed,
SSActionTargetType intParam = SSActionTargetType.Normal,
string strParam = null,
Object objParam = null);
}
SSAction.cs
using UnityEngine;
using System.Collections;
public class SSAction : ScriptableObject {
public bool enable = true;
public bool destroy = false;
public GameObject gameObject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callBack { get; set; }
protected SSAction() { }
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
SSActionManager.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class SSActionManager : MonoBehaviour {
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 Start () {
}
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(kv.Key);
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 gameObj, SSAction action, ISSActionCallback manager) {
for (int i = 0; i < waitingAdd.Count; i++) {
if (waitingAdd[i].gameObject.Equals(gameObj)) {
SSAction ac = waitingAdd[i];
waitingAdd.RemoveAt(i);
i--;
DestroyObject(ac);
}
}
foreach (KeyValuePair<int, SSAction> kv in actions) {
SSAction ac = kv.Value;
if (ac.gameObject.Equals(gameObj)) {
ac.destroy = true;
}
}
action.gameObject = gameObj;
action.transform = gameObj.transform;
action.callBack = manager;
waitingAdd.Add(action);
action.Start();
}
}