作业七:智能巡逻兵
1.要求:
1.1游戏设计要求:
游戏设计要求:
1:创建一个地图和若干巡逻兵(使用动画);
2:每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前 位置为原点计算;
3:巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
4:巡逻兵在设定范围内感知到玩家,会自动追击玩家;
5:失去玩家目标后,继续巡逻;
6:计分:玩家每摆脱一名巡逻兵就加一分
7: 玩家将所有的水晶都收集完则游戏获胜;
8:玩家被捕获,则游戏失败。
1.2程序设计要求:
- 使用订阅与发布模式传递消息:
- 工厂模式生产巡逻兵
2:预制资源制作:
使用的资源包:
[链接](https://assetstore.unity.com/packages/3d/characters/humanoids/character-pack-free-sample-79870)
2.1玩家预制制作:
使用了资源包里的模型预制,只需要进行一定的设置即可:
要点:设置Tag: Player
设置Animator:
1:具体设置:
2:设置Animator:
1:在Assets窗口新建:Create-> Animator Controller:
2:右键击Animator窗口新建状态,点击状态后右击添加转移;
3:在Parameter窗口添加状态转移的变量:
4:点击状态转移的线条,在出现的窗口点击Condition下面的加号,添加转移变量的控制信息;
5:点击状态,在Inspector窗口的motion项目那里添加该状态执行的动作:
Player的Animator具体信息如下:
状态idle的设置:
AnyState->death的设置:
3:player:
2.2 Patrol预制的制作:
因为模型包里只有一个模型,所以Patrol使用的是同一个预制,但是为了区分,将它们的大小做了改变:
设置要点:
1:制作Animator:
1:状态图
2:idle的信息:
3:AnyState->attack转移
2:在预制的第二层basic_rig添加Boxcllide组件:
将Trigger设置为true,同时设置好BoxCollide的Size;这是巡逻兵触发器的范围大小,即他能检测玩家的范围;
3:添加脚本:
一共有三个脚本需要添加到Patrol中,PatrolData.cs和Player.cs添加到第一层,然后PatrolCollide.cs 添加到子对象basic_rig上:
2.3 制作地图:
1:要点是在每个房间添加trigger对象,用来检索player的位置:
2:Trigger的设置:
Is Trigger : true
添加脚本,并设置脚本变量Sign的值,对应相应的房间号。
2.4 预制水晶:
相关的设置如下:
也有一个检测碰撞的脚本:
3:代码:
代码结构图:
3.1订阅发布者模式:
订阅者与发布者模式主要用于处理一些事件,如本游戏中的碰撞事件,以及玩家逃脱追捕这些事件。这些发生事件的类为发布者,即事件的来源;如程序中的CrytalCollision和PlayerCollide;
这些事件都是由一个消息发布媒体来发布,我们称为主题(渠道) ;如GameEvenManage;
订阅者就是对事件感兴趣的类,定义了消息相应的处理方法;对应FirstSceneController;
1:发布者:
- 1:收集水晶:
CrystalCollide:该脚本添加在水晶预制下,用来检测玩家与水晶的碰撞:
如果碰撞者的标签时“player”就发布水晶减少的信息,同时将碰撞后将水晶对象设置为不活跃
public class CrystalCollide : MonoBehaviour
{
void OnTriggerEnter(Collider collider)
{
//如果碰撞者的标签时“player”就发布水晶减少的信息
if (collider.gameObject.tag == "Player" && this.gameObject.activeSelf)
{ //碰撞后将水晶对象设置为不活跃
this.gameObject.SetActive(false);
//减少水晶数量
Singleton<GameEventManager>.Instance.ReduceCrystalNum();
}
}
}
2:玩家被捕获:
该脚本文件添加到Patrol预制对象上:
//如果与玩家相撞,则游戏结束,设置Animator相应的参数变量值。
//Player状态机触发death Trigger,从AnyState转到death状态
//Patrol状态机触发shoot;转入Attack攻击状态。
//同时通过GameEvenManage发布游戏结束的信息。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//本文件添加到Patrol预制对象上
public class PlayerCollide : MonoBehaviour
{
//如果与玩家相撞,则游戏结束,设置Animator相应的参数变量值。
void OnCollisionEnter(Collision other)
{
//当玩家与侦察兵相撞
if (other.gameObject.tag == "Player")
{
//Player状态机触发death Trigger,从AnyState转到death状态
other.gameObject.GetComponent<Animator>().SetTrigger("death");
//Patrol状态机触发shoot;转入Attack攻击状态。
this.GetComponent<Animator>().SetTrigger("shoot");
//发布游戏结束的信息
Singleton<GameEventManager>.Instance.PlayerGameover();
}
}
}
2:主题:GameEvenManage:
所有的事件都通知到该类,由该类向订阅者发布消息:
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 delegate void CrystalEvent();
public static event CrystalEvent CrystalChange;
//玩家逃脱
public void PlayerEscape()
{
if (ScoreChange != null)
{
ScoreChange();
}
}
//玩家被捕
public void PlayerGameover()
{
if (GameoverChange != null)
{
GameoverChange();
}
}
//减少水晶数量
public void ReduceCrystalNum()
{
if (CrystalChange != null)
{
CrystalChange();
}
}
}
3:订阅者:
发布的消息最终会通知到订阅者这里,订阅者定义了相应的方法对发生的事件进行处理:程序中的订阅者是FirstScenceController;下面是订阅者的部分代码:
//订阅事件:并添加相应的处理方法
void OnEnable()
{
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameoverChange += Gameover;
GameEventManager.CrystalChange += ReduceCrystalNumber;
}
//取消订阅该事件
void OnDisable()
{
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameoverChange -= Gameover;
GameEventManager.CrystalChange -= ReduceCrystalNumber;
}
//水晶碰撞的处理方法
void ReduceCrystalNumber()
{
recorder.ReduceCrystal();
}
//分数变化的处理方法
void AddScore()
{
recorder.AddScore();
}
//游戏结束的处理方法:
void Gameover()
{
game_over = true;
patrol_factory.StopPatrol();
action_manager.DestroyAllAction();
}
3.2:添加到预制中的其它代码:
1:巡逻兵检测玩家的脚本: PatrolCollide:该代码添加在Patrol的子对象basic_rig上;
因为Patrol对象为IsTrigger;所以当玩家进入范围时,会触发事件OnTriggerEnter;当玩家离开boxColiide范围时,会触发OnTriggerExit事件;
玩家进入侦察兵追捕范围,
//将跟随玩家设置为true
//并获取玩家的对象信息,赋给PatrolData的player 成员变量
如果玩家摆脱跟随,撤销PatrolDAta中的player信息;
public class PatrolCollide : MonoBehaviour
{
//检测到玩家
void OnTriggerEnter(Collider collider)
{
if (collider.gameObject.tag == "Player")
{
//玩家进入侦察兵追捕范围
//将跟随玩家设置为true
//并获取玩家的对象信息,赋给PatrolData的player成员变量
this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = true;
this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
}
}
void OnTriggerExit(Collider collider)
{
//玩家摆脱跟随,撤销PatrolDAta中的player信息;
if (collider.gameObject.tag == "Player")
{
this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = false;
this.gameObject.transform.parent.GetComponent<PatrolData>().player = null;
}
}
}
2:每个Patrol基本信息的记录:PatrolData,添加在Patrol上:
public class PatrolData : MonoBehaviour
{
public int sign; //标志巡逻兵在哪一块区域
public bool follow_player = false; //是否跟随玩家
public int wall_sign = -1; //当前玩家所在区域标志
public GameObject player; //玩家游戏对象
public Vector3 start_position; //当前巡逻兵初始位置
}
3:玩家位置检测: AreaCollide.cs
该脚本添加在地图的Trigger子对象上,每个房间有一个Trigger,当玩家进入该房间时,触发器出发,更新玩家的位置信息。
玩家进入范围:更新FirstScenceController中的玩家位置信息为当前房间标识;
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.tag == "Player")
{
//更新FirstScenceController中的玩家位置信息
sceneController.wall_sign = sign;
}
}
}
3.3 工厂模式,Factory
用来使用预制批量生产Patrol对象和Crystal对象;通过GetPatrol和GetCrystal两个方法得到批量的对象,通过StopPatrol群体控制patrol 的Animator状态机动作。
public class PropFactory : MonoBehaviour
{
private GameObject patrol = null; //巡逻兵
private List<GameObject> used = new List<GameObject>(); //正在被使用的巡逻兵
private GameObject crystal = null; //水晶
private List<GameObject> usedcrystal = new List<GameObject>(); //正在被使用的水晶
private float range = 12; //水晶生成的坐标范围
private Vector3[] vec = 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++)
{
vec[index] = new Vector3(pos_x[i], 0, pos_z[j]);
index++;
}
}
for(int i=0; i < 9; i++)
{
patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol"));
patrol.transform.position = vec[i];
patrol.GetComponent<PatrolData>().sign = i + 1;
patrol.GetComponent<PatrolData>().start_position = vec[i];
used.Add(patrol);
}
return used;
}
public List<GameObject> GetCrystal()
{
for(int i=0;i<12;i++)
{
crystal = Instantiate(Resources.Load<GameObject>("Prefabs/Crystal"));
float ranx = Random.Range(-range, range);
float ranz = Random.Range(-range, range);
crystal.transform.position = new Vector3(ranx, 0, ranz);
usedcrystal.Add(crystal);
}
return usedcrystal;
}
public void StopPatrol()
{
//切换所有侦查兵的动画
for (int i = 0; i < used.Count; i++)
{
used[i].gameObject.GetComponent<Animator>().SetBool("run", false);
}
}
}
3.4:Patrol的动作脚本:
1:SSAction:
这是巡逻兵所有动作的父类,巡逻兵主要有巡逻和跟踪两个动作,都是从该类派生出去的,便于巡逻兵动作的统一管理和调用:
public class SSAction : ScriptableObject
{
public bool enable = true; //是否正在进行此动作
public bool destroy = false; //是否需要被销毁
public GameObject gameobject; //动作对象
public Transform transform; //动作对象的transform
public ISSActionCallback callback; //动作完成后的消息通知者
protected SSAction() { }
//子类可以使用下面这两个函数
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
2:巡逻动作:GoPatrolAction
按着东北西南的方向顺序,和一定的移动距离进行巡逻,一开始就设置状态机的进入Run动画状态,然后就按特定的路线巡逻。
public class GoPatrolAction : SSAction
{
private enum Dirction { EAST, NORTH, WEST, SOUTH };
private float pos_x, pos_z; //移动前的初始x和z方向坐标
private float move_length; //移动的长度
private float move_speed = 1.2f; //移动速度
private bool move_sign = true; //是否到达目的地
private Dirction dirction = Dirction.EAST; //移动的方向
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(4, 7);
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()
{
this.gameobject.GetComponent<Animator>().SetBool("run", true);
data = this.gameobject.GetComponent<PatrolData>();
}
void Gopatrol()
{
if (move_sign)
{
//不需要转向则设定一个目的地,按照矩形移动
switch (dirction)
{
case Dirction.EAST:
pos_x -= move_length;
break;
case Dirction.NORTH:
pos_z += move_length;
break;
case Dirction.WEST:
pos_x += move_length;
break;
case Dirction.SOUTH:
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.SOUTH)
{
dirction = Dirction.EAST;
}
move_sign = true;
}
}
}
3.追捕动作:PatrolFollowAction:
时刻设置朝向和移动为向着目标玩家:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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);
}
}
4:patrol动作管理类SSActionManage:
处理动作的生成和撤销,进行巡逻或者跟随动作的切换:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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>(); //等待删除的动作的key
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;
}
}
}
3.5:摄像机跟随脚本:CameraFlow:
将摄像机设置为跟随玩家移动,并平滑移动:
public class CameraFlow : MonoBehaviour
{
public GameObject follow; //跟随的物体
public float smothing = 5f; //相机跟随的速度
Vector3 offset; //相机与物体相对偏移位置
void Start()
{
offset = transform.position - follow.transform.position;
}
void FixedUpdate()
{
Vector3 target = follow.transform.position + offset;
//摄像机自身位置到目标位置平滑过渡
transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
}
}
3.6 完整的项目代码:
4:运行:
游戏视频展示如下:
bandicam 2020-12-03 21-43-48-221