Unity3D游戏编程-智能巡逻兵
一、作业要求
游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束。
程序设计要求:
- 必须使用订阅与发布模式传消息
- 工厂模式生产巡逻兵
二、项目配置
Windows 10 Unity 2020.3.17f1c1
三、项目演示
(1)视频演示
点击此处可以前往 可开启字幕观看。
(2)项目下载
下载Assets文件夹 点击此处可以前往gitee
(3)文字说明
- 创建unity专案后,将保存的文件夹中的Assets替换成在上面项目下载的Assets文件夹
- 打开专案,然后点选Assets > Scenes的mySence加载场景
- 运行即可开始游戏
- 角色控制是“WASD”,镜头控制是“↑←↓→”,按下左shift同时进行角色控制可以跑步
- 每逃离一个巡逻兵的追捕加一分,碰撞到巡逻兵就会结束游戏
(4)项目截图
四、前置内容
(1)课程对"模型与动画"的基本练习
为了熟悉操作, 我把本节内容老师给出的例子做了一遍, 详细的内容在下面的博客中。 点击此处前往
(2)MVC模式
MVC模式在每次作业中都是程序的重要结构,具体内容可以参考回去之前的作业: 点击此处前往
(3)工厂模式
工厂模式在以往飞碟游戏中有过应用,此处进行一个重温。 意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。 主要解决:主要解决接口选择的问题。 何时使用:我们明确地计划不同条件下创建不同实例时。 如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。 工厂模式给我感觉有点像线程池,预先创建了一些对象,当需要用的时候才会取出。
(4)Singleton
Unity里的单例模式。一些东西在整个游戏中只有一个而你又想可以方便地随时访问它,这时就可以利用单例模式。 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 主要解决:一个全局使用的类频繁地创建与销毁。 何时使用:当您想控制实例数目,节省系统资源的时候。 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。 在上次飞碟游戏中也有涉及到单例模式。
(5)消息订阅/发布模式
观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
观察者模式包括以下角色:
- 发布者(事件源)/Publisher:事件或消息的拥有者
- 主题(渠道)/Subject:消息发布媒体
- 接收器(地址)/Handle:处理消息的方法
- 订阅者(接收者)/Subscriber:对主题感兴趣的人
观察者模式的特点:
- 发布者与订阅者没有直接的耦合
- 发布者与订阅者没有直接关联,可以 多对多 通讯
- 在MVC设计中,是实现模型与视图分离的重要手段
引用课程的例子:明星希望把自己的动态及时通知粉丝,但她(他)并不喜欢一一通知粉丝,所以用 微信公众号 或 微博 通知粉丝,这样,粉丝仅需知道明星的公众号,然后关注它。
- Receiver/Subscriber(粉丝) 依赖 具体的公众号Subject(通过Handle接收),而不是 sender/Publisher。
- 一个公众号可以自动通知很多 receivers
而在多数场景中(Handle的) listener 接口仅包含一个接受消息的方法,因此 C# 对这种情况下的订阅/发布模式做了语言级别的实现,称为事件-代理机制。
对于消息发送方: 对于消息接收方:
五、实现过程和方法(算法)
(1)巡逻兵
(1-1)GuardData
一个巡逻兵所包含的参数有:
public class GuardData : MonoBehaviour {
public GameObject model;
public float walkSpeed = 1.3f;
public float runSpeed = 1.8f;
public int sign;
public bool isFollow = false;
public int playerSign = -1;
public Vector3 start_position;
[SerializeField]
private Animator anim;
private Rigidbody rigid;
void Awake() {
anim = model.GetComponent<Animator>();
rigid = GetComponent<Rigidbody>();
}
public void OnGround() {
anim.SetBool("OnGround", true);
}
public void OnGroundEnter() {
}
}
(1-2)GuardFactory
巡逻兵工厂创建一组巡逻兵对象, 因此需要有一个队列放置创建好的巡逻兵object并且用作返回。
public class GuardFactory : MonoBehaviour {
private GameObject guard = null;
private List<GameObject> used = new List<GameObject>();
private Vector3[] vec = new Vector3[9];
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++) {
vec[index] = new Vector3(pos_x[i], 0, pos_z[j]);
index++;
}
}
for(int i = 0; i < 8; i++) {
guard = Instantiate(Resources.Load<GameObject>("Prefabs/Guard"));
guard.transform.position = vec[i];
guard.GetComponent<GuardData>().sign = i + 1;
guard.GetComponent<GuardData>().start_position = vec[i];
guard.GetComponent<Animator>().SetFloat("forward", 1);
used.Add(guard);
}
return used;
}
}
(1-3)巡逻兵的行为
GuardActionManager对巡逻兵的动作类进行了调用:
public class GuardActionManager : SSActionManager, ISSActionCallback {
private GuardPatrolAction patrol;
private GameObject player;
public void GuardPatrol(GameObject guard, GameObject _player) {
player = _player;
patrol = GuardPatrolAction.GetSSAction(guard.transform.position);
this.RunAction(guard, patrol, this);
}
public void SSActionEvent(
SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, GameObject objectParam = null) {
if (intParam == 0) {
GuardFollowAction follow = GuardFollowAction.GetSSAction(player);
this.RunAction(objectParam, follow, this);
} else {
GuardPatrolAction move = GuardPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<GuardData>().start_position);
this.RunAction(objectParam, move, this);
Singleton<GameEventManager>.Instance.PlayerEscape();
}
}
}
巡逻兵动作实际上在GuardPatrolAction.cs与GuardFollowAction.cs中实现。
GuardPatrolAction使得巡逻兵进行巡逻。
- 初始化的时候, GuardPatrolAction加载参数, 并且让巡逻兵播放行走的动画
- GuardPatrolAction的动作产生会随机出一个边长数值,令巡逻兵沿着某个方向走随机距离
- GuardPatrolAction的fixupdate会使得巡逻兵开始巡逻, 当player当前所在格与巡逻兵的所在格相同时, 因为巡逻兵的动作变更, 巡逻结束, 返回动作完成信息。
GuardFollowAction使得巡逻兵进行追逐。
- 初始化的时候, GuardFollowAction加载参数, 并且让巡逻兵播放奔跑的动画
- GuardFollowAction令巡逻兵朝着(Lookat)player的方向移动
- 如果player脱离了当前巡逻兵所在的格子, 则巡逻兵的动作变更, 追逐结束, 返回动作完成信息。
(2)玩家player对象
(2-1)playerInput
对于player对象以及镜头的控制,首先要得到键盘的输入,这部分的内容在playerInput.cs中。
- 判断键盘"W/S"有无按下, 调整前后平移的参数
- 判断键盘"A/D"有无按下, 调整左右平移的参数
- 判断键盘"↑/↓"有无按下, 调整镜头上下移动的参数
- 判断键盘"←/→"有无按下, 调整镜头左右移动的参数
- 判断键盘“left shift”有无按下
然后ActorController类, 就会根据playerInput里面的参数(主要依赖参数Dmag、Dvec、run), 执行对player对象的移动控制和动画的播放。 而CameraContoller类则会根据playerInput里面关于镜头的参数(主要依赖参数Jup、Jright), 执行对镜头对象的移动。
(2-2)ActorController
ActorController根据参数调整动画混合树,使得player对象进行行走或奔跑动作,并且计算移动距离的向量。
void Update() {
float targetRunMulti = pi.run ? 2.0f : 1.0f;
anim.SetFloat("forward", pi.Dmag * Mathf.Lerp(anim.GetFloat("forward"), targetRunMulti, 0.3f));
if(pi.Dmag > 0.01f) {
Vector3 targetForward = Vector3.Slerp(model.transform.forward, pi.Dvec, 0.2f);
model.transform.forward = targetForward;
}
if(!lockPlanar) {
planarVec = pi.Dmag * model.transform.forward * walkSpeed * (pi.run ? runMultiplier : 1.0f);
}
}
(2-3)CameraContolle
CameraContolle除了要根据键盘的输入,调整镜头角度之外,还要调整player的朝向。
void FixedUpdate() {
Vector3 tempModelEuler = model.transform.eulerAngles;
playerHandle.transform.Rotate(Vector3.up, pi.Jright * horizontalSpeed * Time.fixedDeltaTime);
tempEulerX -= pi.Jup * verticalSpeed * Time.fixedDeltaTime;
tempEulerX = Mathf.Clamp(tempEulerX, -35, 30);
cameraHandle.transform.localEulerAngles = new Vector3(tempEulerX, 0, 0);
model.transform.eulerAngles = tempModelEuler;
camera.transform.position = Vector3.SmoothDamp(
camera.transform.position, transform.position,
ref cameraDampVelocity, cameraDampValue);
camera.transform.eulerAngles = transform.eulerAngles;
}
(3)AreaCollide
AreaCollide 负责侦测玩家当前进入到哪一个格子, 修改参数playerSign(这个参数在FirstSceneController中), 由此别的函数侦测到Sign的变化, 对player进行追逐。
(4)PlayerCollide
当玩家碰撞到巡逻兵时,要求发布事件PlayerGameover。
(5)Director
Director的建立基本是默认的单例、懒汉模式,与之前的作业程序编写一样。 Director在本次作业的程序SSDirector.cs中。
(6)FirstSceneController
FirstSceneController一如既往地负责游戏第一个场景的布景、演员的上下场、管理动作。 FirstSceneController除了继承了MonoBehaviour,还继承了IUserAction、ISceneController,在文件Interface.cs就有这两个抽象类的描述:
public interface ISceneController {
void LoadResources();
}
public interface IUserAction {
int GetScore();
bool GetGameover();
void Restart();
}
FirstSceneController除了要对游戏第一场景进行初始化,还要实现上面这些接口。 FirstSceneController负责管理以下这些参数以及他们相关的动作。
public GuardFactory guard_factory;
public ScoreRecorder recorder;
public GuardActionManager action_manager;
public int playerSign = -1;
public GameObject player;
public UserGUI gui;
private List<GameObject> guards;
private bool game_over = false;
FirstSceneController的Awake()函数就对以上这些参数进行初始化,并且加载资源:
void Awake() {
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
guard_factory = Singleton<GuardFactory>.Instance;
action_manager = gameObject.AddComponent<GuardActionManager>() as GuardActionManager;
gui = gameObject.AddComponent<UserGUI>() as UserGUI;
LoadResources();
recorder = Singleton<ScoreRecorder>.Instance;
}
public void LoadResources() {
Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));
player = Instantiate(
Resources.Load("Prefabs/Player"),
new Vector3(10, 0, -10), Quaternion.identity) as GameObject;
guards = guard_factory.GetPatrols();
for (int i = 0; i < guards.Count; i++) {
action_manager.GuardPatrol(guards[i], player);
}
}
其他的函数:
- GetScore()返回recorder的GetScore()函数结果, 返回分数
- GetGameover()直接返回参数的game_over
- Restart()直接加载游戏初始状态的场景
(7)ScoreRecorder
ScoreRecorder就是拿来记录分数, 增加分数。
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++;
}
}
(8)代理机制-消息发布:GameEventManager
GameEventManager负责发布事件,它实现了Interface.cs中的:
public interface IGameStatusOp {
void PlayerEscape();
void PlayerGameover();
}
负责发布游戏的两种状态消息, 第一种是玩家逃离巡逻兵, 第二种是玩家被巡逻兵逮捕。 第一种情况, 需要修改得分 第二种情况, 需要修改bool-game_over
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();
}
}
}
对应地,消息的订阅者就是FirstSceneController,FirstSceneController根据消息,控制游戏状态。
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;
}
六、参考资料
- 【Unity3D】智能巡逻兵
- 【Unity技巧】使用单例模式Singleton
- 单例模式\工厂模式
|