文章目录
1、智能巡逻兵
- 提交要求:
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边形,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分: 玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
- subject: OnLostGoal
- Publisher: ?
- Subscriber: ?
- 工厂模式生产巡逻兵
- 友善提示1: 生成 3~5个边的凸多边形
- 随机生成矩形
- 在矩形每个边上随机找点,可得到 3 - 4 的凸多边形
- 5 ?
- 友善提示2: 参考以前博客,给出自己新玩法
1) 订阅和发布模式
(1) 介绍
订阅和发布模式(Publish–subscribe pattern)属于行为型模式,如同讲义上所说,公众号的管理就属于订阅和发布模式,而我们在网购、看视频也会有类似的操作(收藏、关注),这些也是一样的道理。
消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者),而是通过调度中心广播出去,让订阅该消息主题的订阅者消费到。
结合讲义中的图片(如下图),这里明星就是发布者,粉丝是订阅者,明显并不直接跟粉丝沟通而是发布到公众号,有订阅该明星公众号的粉丝就会收到明星发布的主题。
(2) 与观察者模式的区别
在课上学习到订阅和发布模式的时候,我第一反应就是"这不就是观察者模式嘛",直接:
目
标
观
察
者
模
式
=
发
布
者
订
阅
发
布
模
式
观
察
者
观
察
者
模
式
=
订
阅
者
订
阅
发
布
模
式
\begin{aligned} 目标_{观察者模式} &= 发布者_{订阅发布模式}\\ 观察者_{观察者模式} &= 订阅者_{订阅发布模式} \end{aligned}
目标观察者模式观察者观察者模式=发布者订阅发布模式=订阅者订阅发布模式
甚至我在网上查询资料的时候也有很多博文说两者指的是同一个设计模式,但是后来在网上浏览比较久的资料,发现其实这两种模式只能说非常相似,但它们还是有区别的。
如上面这张图所示,虽然这两种模式都实现了信息的即时通知,但是两者调度的地方有所不同:
- 观察者模式: 目标发生变化的时候,会主动调用所有观察者的更新方法(notify),目标和观察者之间是松耦合的,两者间存在依赖。
- 订阅发布模式: 当发布者发生变化的时候,他会将主题发送给一个独立的调度中心,由调度中心决定分发到哪些订阅者处,发布者和订阅者是完全解耦的,两者间通过调度中心(通常设计成消息中间件)产生联系。
2) 项目分析与准备
(1) Unity项目创建
用在Unity创建一个3D项目,命名为Patrol,进入之后我们创建一个空对象用于加载我们的脚本:首先按Ctrl+Shift+N创建空对象,然后在游戏对象树上右键点击它选择重命名,将其重命名为"main"用于加载我们的脚本
(2) 事物
根据项目的要求,我们的场景除了脚本控制外还需要完成各种预制的制作,存到Assets/Resources文件夹中。并且完成各脚本的编写,具体代码见下一小节内容
-
金币
金币的话我们同样从资源商店中下载资源,搜索Gold Coins
,选择第1个免费资源,添加资源后在本地下载导入,我们这个项目只需要金币,因此实际上只需要导入跟单个金币有关的部分内容,整个过程如下图所示(这里显示完整的导入过程,后面的资源就不再详述,另外建议导入后拖动到Resources文件夹内,保持Assets的整洁)。
在导入之后我们对金币预制进行适当修改,最终成品如下图所示:
-
巡逻兵
巡逻兵的话,我们从资源商店中找到名为Character Pack: Zombie Sample,按金币的流程导入到项目中,这里我们导入全部的内容。
另外我们需要为其添加动画,最终配置的Animator视图以及整个巡逻兵的形象如下:
-
玩家
玩家的形象我们同样使用同个开发者设计的人物,点此处可跳转到商店查看,最终对玩家预制复制后并进行适当修改,加上动作控制器,最终效果如下图:
-
地图
地图这里我们是仿照师兄师姐的设计进行仿制,同样是九宫格的设计,每个区域各有一个trigger(isTrigger
为真,并且具有Box Collider并绑定AreaCollide.cs
脚本),并且还放置了我们上面导入的金币,最终效果如下图:
- 由于篇幅原因,每个预制的实际流程并没有在文章中体现,建议直接从仓库中下载整个Assets文件夹,覆盖到自己仓库重新实现。
(3) 类设计
这次用到的新知识点不多,主要在于订阅发布模式的设计,因此我们根据讲义中给的局部UML图完成订阅发布模式的设计。
而对于整个项目,我们仍使用MVC完成项目架构,最终项目脚本文件夹的截图如下所示:
最终将FirstSceneController.cs
脚本拖到main对象上即可运行启动。
3) 代码设计
(2)Models —— PatrolModel.cs
对于本项目,Model中只有巡逻兵的model——PatrolModel
,它保存巡逻兵的部分属性(所在区域/是否跟随玩家)以及控制着巡逻兵的一部分基本设置,首先开始的时候冻结它的选组换,然后在运行的时候检查其高度和欧拉角的z值,保证非0。
using UnityEngine;
public class PatrolModel : MonoBehaviour
{
public int block;
public bool follow_player = false;
private void Start()
{
if (gameObject.GetComponent<Rigidbody>())
{
gameObject.GetComponent<Rigidbody>().freezeRotation = true;
}
}
void Update()
{
if (this.gameObject.transform.localEulerAngles.x != 0 || gameObject.transform.localEulerAngles.z != 0)
{
gameObject.transform.localEulerAngles = new Vector3(0, gameObject.transform.localEulerAngles.y, 0);
}
if (gameObject.transform.position.y != 0)
{
gameObject.transform.position = new Vector3(gameObject.transform.position.x, 0, gameObject.transform.position.z);
}
}
}
(2)Views —— UserGUI.cs
Views层这次也依然只有UserGUI.cs
文件,这次这个文件不再用于FirstSceneController
中,而是附加到Player预制中的Camera上,用于显示一些信息以及控制摄像机跟随玩家移动,具体代码如下
using UnityEngine;
public class UserGUI : MonoBehaviour
{
IUserAction controller;
ISceneController SceneController;
CCActionManager CCManger;
public GameObject t;
bool isWin = false;
float S;
PatrolFactory PF;
// Font Style
GUIStyle fontStyle = new GUIStyle();
void Start()
{
controller = SSDirector.getInstance().currentSceneController as IUserAction;
SceneController = SSDirector.getInstance().currentSceneController as ISceneController;
S = Time.time;
// Font Style
fontStyle.alignment = TextAnchor.MiddleCenter;
fontStyle.fontSize = 30;
fontStyle.normal.textColor = Color.white;
}
private void OnGUI()
{
// Screen and items size
float w = Screen.width;
float h = Screen.height;
float field_w = w * 0.2f;
float field_h = h * 0.1f;
if (!SSDirector.getInstance().isRunning) S = Time.time;
GUI.Label(new Rect(w * 0.05f, h * 0.05f, field_w, field_h), "Coin: " + SceneController.GetCoinCount(), fontStyle);
GUI.Label(new Rect(w * 0.35f, h * 0.05f, field_w, field_h), "Score: " + controller.GetScore(), fontStyle);
GUI.Label(new Rect(w * 0.65f, h * 0.05f, field_w, field_h), "Time: " + ((int)(Time.time - S)), fontStyle);
if (SSDirector.getInstance().isRunning)
{
if (!controller.GetGameState())
{
SSDirector.getInstance().isRunning = false;
}
if (SceneController.GetCoinCount() >= 9)
{
isWin = true;
SSDirector.getInstance().isRunning = false;
}
}
else
{
if (isWin)
{
GUI.Label(new Rect(w * 0.4f, h * 0.45f, field_w, field_h), "Win!", fontStyle);
PF = PatrolFactory.PF;
PF.StopPatrol();
if (GUI.Button(new Rect(w * 0.4f, h * 0.45f, field_w, field_h), "EXIT"))
{
UnityEditor.EditorApplication.isPlaying = false;
}
}
else if (GUI.Button(new Rect(w * 0.4f, h * 0.45f, field_w, field_h), "Start"))
{
SSDirector.getInstance().isRunning = true;
SceneController.LoadResources();
S = Time.time;
controller.Restart();
}
}
}
private void Update()
{
// Get Offset
float translationX = Input.GetAxis("Horizontal");
float translationZ = Input.GetAxis("Vertical");
// Move Character
controller.MovePlayer(translationX, translationZ);
}
}
(3)Controller
a. Collide —— AreaCollide.cs
这部分用于控制9个区域的碰撞体,当玩家进入区域时触发相应的操作
using UnityEngine;
public class AreaCollide : MonoBehaviour
{
public int sign = 0;
FirstSceneController sceneController;
private void Start()
{
sceneController = SSDirector.getInstance().currentSceneController as FirstSceneController;
}
void OnTriggerEnter(Collider collider)
{
if (collider.gameObject.tag == "Player")
{
sceneController.SetPlayerArea(sign);
GameEventManager.Instance.PlayerEscape();
}
}
}
b. Collide —— CoinCollide.cs
该部分脚本用于绑定在金币上,让玩家触碰到后能够触发收取金币。
using UnityEngine;
public class CoinCollide : MonoBehaviour
{
void OnCollisionEnter(Collision other)
{
SSDirector director;
if (other.gameObject.tag == "Player")
{
Destroy(this.gameObject);
director = SSDirector.getInstance();
director.currentSceneController.AddCoin();
}
}
}
c. Collide —— PatrolCollide.cs
这部分实际上是绑定在巡逻兵(即丧尸)身上的,这样当玩家遇上丧尸的时候就会失败
using UnityEngine;
public class PatrolCollide : MonoBehaviour
{
void OnCollisionEnter(Collision other)
{
if (other.gameObject.tag == "Player")
{
GameEventManager.Instance.PlayerGameover();
}
}
}
d. Action —— CCTracertAction.cs
这个用于玩家到达巡逻兵区域的时候触发追随机制,让巡逻兵追踪玩家,直至玩家离开区域
using UnityEngine;
public class CCTracertAction : SSAction
{
public GameObject target;
public float speed;
private CCTracertAction() { }
public static CCTracertAction getAction(GameObject target, float speed)
{
CCTracertAction action = ScriptableObject.CreateInstance<CCTracertAction>();
action.target = target;
action.speed = speed;
return action;
}
public override void Update()
{
this.transform.position = Vector3.MoveTowards(transform.position, target.transform.position, speed * Time.deltaTime);
Quaternion rotation = Quaternion.LookRotation(target.transform.position - gameObject.transform.position, Vector3.up);
gameObject.transform.rotation = rotation;
if (gameObject.GetComponent<PatrolModel>().follow_player == false || transform.position == target.transform.position)
{
this.destory = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
e. Action —— CCActionManager.cs
我们这里的作用跟之前的作业一样,都是通过运动学计算巡逻兵的运动。在这里对巡逻兵的运动进行管理,Tracert
用于追踪玩家,GoAround
则是正常游荡
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback
{
public SSActionEventType Complete = SSActionEventType.Competeted;
Dictionary<int, CCMoveToAction> actionList = new Dictionary<int, CCMoveToAction>();
public void Tracert(GameObject p, GameObject player)
{
if (actionList.ContainsKey(p.GetComponent<PatrolModel>().block)) actionList[p.GetComponent<PatrolModel>().block].destory = true;
CCTracertAction action = CCTracertAction.getAction(player, 0.8f);
RunAction(p.gameObject, action, this);
}
public void GoAround(GameObject p)
{
CCMoveToAction action = CCMoveToAction.GetSSAction(p.GetComponent<PatrolModel>().block, GetNewTarget(p), 0.6f);
actionList.Add(p.GetComponent<PatrolModel>().block, action);
RunAction(p.gameObject, action, this);
}
private Vector3 GetNewTarget(GameObject p)
{
Vector3 pos = p.transform.position;
int block = p.GetComponent<PatrolModel>().block;
float ZUp = 13.2f - (block / 3) * 9.65f;
float ZDown = 5.5f - (block / 3) * 9.44f;
float XUp = -4.7f + (block % 3) * 8.8f;
float XDown = -13.3f + (block % 3) * 10.1f;
Vector3 Move = new Vector3(Random.Range(-2f, 2f), 0, Random.Range(-2f, 2f));
Vector3 Next = pos + Move;
while (!(Next.x < XUp && Next.x > XDown && Next.z < ZUp && Next.z > ZDown))
{
Move = new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f));
Next = pos + Move;
}
return Next;
}
public void StopAll()
{
foreach (CCMoveToAction x in actionList.Values)
{
x.destory = true;
}
actionList.Clear();
}
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0,
string strParam = null,
object objectParam = null)
{
if (actionList.ContainsKey(source.gameObject.GetComponent<PatrolModel>().block)) actionList.Remove(source.gameObject.GetComponent<PatrolModel>().block);
GoAround(source.gameObject);
}
}
f. Interface —— ISceneController.cs
这里接口声明的方法较之前几次作业又有了一些变化,只留下LoadResource
,将其他的变成addCoin
和GetCoinCount
两个方法,用于针对金币做出行为。
public interface ISceneController
{
void AddCoin();
int GetCoinCount();
void LoadResources();
}
g. Interface —— IUserAction.cs
public interface IUserAction
{
int GetScore();
void Restart();
bool GetGameState();
void MovePlayer(float translationX, float translationZ);
}
h. FirstSceneController.cs
核心的部分代码还是在FirstSceneController的手上,实现整个场景的大致控制
using System.Collections.Generic;
using UnityEngine;
public class FirstSceneController : MonoBehaviour, ISceneController, IUserAction
{
GameObject player = null;
PatrolFactory PF;
int score = 0;
int areaIndex = 4;
bool gameState = false;
int coinCount = 0;
Dictionary<int, GameObject> allPatrol = null;
CCActionManager CCManager = null;
public void AddCoin()
{
coinCount++;
}
public int GetCoinCount()
{
return coinCount;
}
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
PF = PatrolFactory.PF;
if (CCManager == null)
{
CCManager = gameObject.AddComponent<CCActionManager>();
}
if (player == null && allPatrol == null)
{
Instantiate(Resources.Load<GameObject>("Prefabs/world"), new Vector3(0, 0, 0), Quaternion.identity);
player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(0, 0, 0), Quaternion.identity) as GameObject;
allPatrol = PF.GetPatrols();
}
if (player.GetComponent<Rigidbody>())
{
player.GetComponent<Rigidbody>().freezeRotation = true;
}
}
void Update()
{
if (player.transform.localEulerAngles.x != 0 || player.transform.localEulerAngles.z != 0)
{
player.transform.localEulerAngles = new Vector3(0, player.transform.localEulerAngles.y, 0);
}
if (player.transform.position.y <= 0)
{
player.transform.position = new Vector3(player.transform.position.x, 0, player.transform.position.z);
}
}
void OnEnable()
{
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameoverChange += Gameover;
}
void OnDisable()
{
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameoverChange -= Gameover;
}
public void LoadResources()
{
}
public int GetScore()
{
return score;
}
public void Restart()
{
player.GetComponent<Animator>().Play("idle");
PF.StopPatrol();
gameState = true;
score = 0;
player.transform.position = new Vector3(0, 0, 0);
allPatrol[areaIndex].GetComponent<PatrolModel>().follow_player = true;
CCManager.Tracert(allPatrol[areaIndex], player);
foreach (GameObject x in allPatrol.Values)
{
if (!x.GetComponent<PatrolModel>().follow_player)
{
CCManager.GoAround(x);
}
}
}
public bool GetGameState()
{
return gameState;
}
public void SetPlayerArea(int x)
{
if (areaIndex != x && gameState)
{
allPatrol[areaIndex].GetComponent<Animator>().SetBool("run", false);
allPatrol[areaIndex].GetComponent<PatrolModel>().follow_player = false;
areaIndex = x;
}
}
void AddScore()
{
if (gameState)
{
++score;
allPatrol[areaIndex].GetComponent<PatrolModel>().follow_player = true;
CCManager.Tracert(allPatrol[areaIndex], player);
allPatrol[areaIndex].GetComponent<Animator>().SetBool("run", true);
}
}
void Gameover()
{
CCManager.StopAll();
allPatrol[areaIndex].GetComponent<PatrolModel>().follow_player = false;
allPatrol[areaIndex].GetComponent<Animator>().SetTrigger("attack");
player.GetComponent<Animator>().SetTrigger("death");
gameState = false;
}
public void MovePlayer(float translationX, float translationZ)
{
if (gameState && player != null)
{
if (translationZ != 0)
{
player.GetComponent<Animator>().SetBool("run", true);
}
else
{
player.GetComponent<Animator>().SetBool("run", false);
}
player.transform.Translate(0, 0, translationZ * 4f * Time.deltaTime);
player.transform.Rotate(0, translationX * 50f * Time.deltaTime, 0);
}
}
}
i. PatrolFactory.cs
这里跟上一次作业一样采用工厂模式管理巡逻兵的生成,减少对象的频繁生成和销毁带来的性能损耗
using System.Collections.Generic;
using UnityEngine;
public class PatrolFactory
{
public static PatrolFactory PF = new PatrolFactory();
private Dictionary<int, GameObject> used = new Dictionary<int, GameObject>();
private PatrolFactory() {}
int[] xPos = { -7, 0, 7 };
int[] zPos = { 8, 2, -8 };
public Dictionary<int, GameObject> GetPatrols()
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
GameObject newPatrol = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Patrol"));
newPatrol.AddComponent<PatrolModel>();
newPatrol.transform.position = new Vector3(xPos[j], 0, zPos[i]);
newPatrol.GetComponent<PatrolModel>().block = i * 3 + j;
newPatrol.SetActive(true);
used.Add(i * 3 + j, newPatrol);
}
}
return used;
}
public void StopPatrol()
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
used[i * 3 + j].transform.position = new Vector3(xPos[j], 0, zPos[i]);
}
}
}
}
j. GameEventManager.cs
这一部分应该是跟之前项目最不一样的地方,这部分采用了发布订阅这种设计模式,实现了分数变化和游戏结束这两个变动事件通知,具体代码如下:
public class GameEventManager
{
public static GameEventManager Instance = new GameEventManager();
/* Publisher/Subscriber Pattern */
// Score
public delegate void ScoreEvent();
public static event ScoreEvent ScoreChange;
// GameOver
public delegate void GameoverEvent();
public static event GameoverEvent GameoverChange;
private GameEventManager() { }
// Escape
public void PlayerEscape()
{
if(ScoreChange != null)
{
ScoreChange();
}
}
// Caught
public void PlayerGameover()
{
if(GameoverChange != null)
{
GameoverChange();
}
}
}
其他
除了上面说的这些外,Controllers层还有其他一些我们前几次项目出现了很多次的类,为节省篇幅我们这里就不再展示,下面这些具体省略的类可以查看我之前的博客。
- SSDirector.cs
- SSActionManager.cs
- SSAction.cs
- ISSActionCallback.cs
- CCMoveToAction.cs
4) 项目呈现
最终将脚本FirstSceneController.cs
挂在空对象main上,点击运行即可开始游戏,具体运行动图如下:
所有资源和脚本(即整个Assets文件夹)可从此处下载
2、体会
这次作业实际上也不是非常难,但是最近作业比较多所以做得并不是很完美,个人觉得很多地方还有待完善,像地图,玩家等等都可以进行适当的调整。另外这次作业里面新出现的当属GameEventManager
,虽然代码比较简短,但是发布订阅模式这个内容还是非常重要的,这次作业也让我发现了它和观察者模式的些许区别。