本文是3D游戏编程与设计第七次作业的博客,内容为编写游戏“巡逻兵(Patrol Man)”
目录
任务说明
编写一个智能巡逻兵游戏
游戏内容要求
游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计要求:
- 必须使用订阅与发布模式传消息
- 工厂模式生产巡逻兵
成果说明
项目地址—Gitee
预制说明
迷宫采用 3*3 结构,由九个正方形组成,每个迷宫上挂载 AreaCollide.cs,当玩家进入后通知场记更新玩家位置信息
Guard 与 Player 采用了潘老师推荐的资源 YBoy
依照步骤配置完成后,我感觉原地跳与下落两个动作在这种追逐游戏中不太实用,所以对动作进行改进,即修改动画控制器 Player Controller 为(同时需要删除代码文件中的相关部分,主要为其消息发布函数)
同时在jump -> roll中设置exit time,缩短跳跃动作持续的时间,ground 动作混合树维持不变
此时以翻滚作为跳跃的着地动作,提高游戏中跳跃的可玩性
代码说明
注:框架部分的代码,如SSAction.cs、SSActionManager.cs、Interface.cs等此前已多次使用,其说明可以参考上次作业的博客,此处仅就与本次作业相关的部分进行说明
GuardData
保存了巡逻兵的基本数据
public GameObject model;
public float walkSpeed; //巡逻速度
public float runSpeed; //追逐速度
public int sign; //标志巡逻兵在哪一块区域
public bool isFollow = false; //是否跟随玩家
public int playerSign = -1; //当前玩家所在区域标志
public Vector3 start_position; //当前巡逻兵初始位置
GuardFactory
巡逻兵工厂类,由位置对巡逻兵进行初始化并储存
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;
}
GuardPatrolAction&GuardFollowAction
巡逻兵的巡逻动作类与追逐动作类。当巡逻兵巡逻时,不断检测玩家是否进入自己所在迷宫,是则利用回调函数(在GuardActionManager中实现,第三个参数即为动作的标志位)开始追逐玩家;巡逻机制则为不断向矩形的顶点移动,每当到达时转向,以实现在矩形路线上的移动
/* public class GuardPatrolAction */
public override void FixedUpdate() {
//巡逻
Gopatrol();
//玩家进入该区域,巡逻结束,开始追逐
if (data.playerSign == data.sign) {
this.destroy = true;
this.callback.SSActionEvent(this, SSActionEventType.Competeted, 0, this.gameobject);
}
}
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) {
rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z);
} else {
dirction = dirction + 1;
if(dirction > Dirction.SOUTH) {
dirction = Dirction.EAST;
}
move_sign = true;
}
}
当巡逻兵追逐时,则不断向玩家方向移动,同时检测玩家是否已离开自己所在迷宫,是则回调巡逻动作
/* public class GuardFollowAction */
public override void FixedUpdate() {
transform.LookAt(player.transform.position);
//rigid.velocity = new Vector3(planeVec.x, rigid.velocity.y, planeVec.z);
rigid.velocity = planeVec;
//如果玩家脱离该区域则继续巡逻
if (data.playerSign != data.sign) {
this.destroy = true;
this.callback.SSActionEvent(this, SSActionEventType.Competeted, 1, this.gameobject);
}
}
ActorController
这是角色控制类,控制角色的动画播放与移动机制
//刷新每秒60次
void Update() {
//修改动画混合树
/*1.从走路到跑步没有过渡*/
/*anim.SetFloat("forward", pi.Dmag * (pi.run ? 2.0f : 1.0f));*/
/*2.使用Lerp加权平均解决*/
float targetRunMulti = pi.run ? 2.0f : 1.0f;
anim.SetFloat("forward", pi.Dmag * Mathf.Lerp(anim.GetFloat("forward"), targetRunMulti, 0.3f));
//播放翻滚动画
if (rigid.velocity.magnitude > 1.0f) {
anim.SetTrigger("roll");
}
//播放跳跃动画
if (pi.jump) {
anim.SetTrigger("jump");
}
//转向
if(pi.Dmag > 0.01f) {
/*1.旋转太快没有补帧*/
/*model.transform.forward = pi.Dvec;*/
/*2.使用Slerp内插值解决*/
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);
}
}
//物理引擎每秒50次
private void FixedUpdate() {
//Time.fixedDeltaTime 50/s
//1.修改位置
//rigid.position += movingVec * Time.fixedDeltaTime;
//2.修改速度
rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z) + thrustVec;
//一帧
thrustVec = Vector3.zero;
}
订阅与发布模式
GameEventManager 发布消息,其中 PlayerEscape 由巡逻兵由追逐回调巡逻动作时发出,PlayerGameover 由 PlayerCollide 检测到玩家与巡逻兵相撞时发出
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 订阅消息,在消息发出后就会调用之前订阅了它的方法
void OnEnable() {
GameEventManager.ScoreChange += AddScore;
GameEventManager.ScoreChange += AddRunSpeed;
GameEventManager.GameoverChange += Gameover;
}
void OnDisable() {
GameEventManager.ScoreChange -= AddScore;
GameEventManager.ScoreChange -= AddRunSpeed;
GameEventManager.GameoverChange -= Gameover;
}
void AddScore() {
recorder.AddScore();
}
void AddRunSpeed(){
guard_factory.patrolsFaster();
}
void Gameover() {
game_over = true;
}
改进说明
- 增加每逃脱一次巡逻兵速度增加一次的规则
在 GameEventManager 中为 AddRunSpeed 方法订阅 PlayerEscape 消息,在工厂类 GuardFactory 中增加方法 patrolsFaster,因为 GuardFollowAction 中每次巡逻速度初始化依据 GuardData 的 runSpeed,所以可以实现每次逃脱后巡逻速度的增加
/* FirstSceneController */
void AddRunSpeed(){
guard_factory.patrolsFaster();
}
void OnEnable() {
...
GameEventManager.ScoreChange += AddRunSpeed;
}
/* GuardFactory */
private List<GameObject> used = new List<GameObject>(); //正在使用的巡逻兵列表
public void patrolsFaster(){
foreach (GameObject guardi in used)
{
guardi.GetComponent<GuardData>().runSpeed += 2.0f;
}
}
- 增加上帝视角模式
在场景中新建 Camera 命名为 HeadCamera,并调整位置使其可以俯视整个迷宫;在 CameraController 中增加标志位bool isAfterPlayer
,标志当前视角模式,同时存储新建的 HeadCamera,然后依据该标志位决定采用哪个 Camera 作为实际游戏视角
/* CameraController */
private GameObject headCamera;
void Awake() {
...
headCamera = GameObject.Find("HeadCamera");
}
void FixedUpdate() {
if(isAfterPlayer){
headCamera.GetComponent<Camera>().enabled = false;
camera.GetComponent<Camera>().enabled = true;
} else {
headCamera.GetComponent<Camera>().enabled = true;
camera.GetComponent<Camera>().enabled = false;
}
...
}
而标志位 isAfterPlayer 将在“切换视角”按钮点击后由 UserGUI 通知场记 FirstSceneController,然后由其调用 CameraController 的接口进行改变,实现视角切换
成果展示
正常游戏
动作展示
视角切换
参考博客 学长/学姐Tifinity