前言
中山大学数据科学与计算机学院3D游戏课程学习记录博客。
游戏代码: gitee
游戏视频: bilibili
参考师兄的博客: 师兄博客
游戏要求
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 必须使用订阅与发布模式传消息;
- 工厂模式生产巡逻兵。
游戏分析
- 订阅与发布模式:
subject:OnLostGoal
Publisher:GameEventManager
Subscriber:FirstSceneController
发布者是消息或信息的拥有者,订阅者是请求信息的类。
-
人物模型:
人物模型处理见博客:人物模型 -
代码结构:
代码大致分为四个部分:玩家和巡逻兵的生成(这部分放到人物模型博客中)、玩家和巡逻兵追逐逻辑(包括动作管理)、游戏场景的逻辑(使用订阅与发布模式)、UI界面的生成(包括分数的计算)、。
所以本文从这四个方面来设计代码。
游戏实现
一些文件在上次代码的基础上进行小幅度改动或者无改动,可以在本次作业中使用。比如计分器、单例模式、动作管理器等相关代码。
1.玩家和巡逻兵的生成:人物模型
2.玩家和巡逻兵追逐逻辑:
这部分代码需要完成动作管理和追逐逻辑两部分。
动作管理器需要控制巡逻兵的动作;
生成巡逻兵追逐玩家的动作;
生成玩家移动的动作;
生成巡逻兵继续巡逻的动作;
调用产生的动作。
- 巡逻兵追逐玩家:PatrolFollowAction.cs
追逐玩家时需要三个变量来记录玩家位置,巡逻兵速度和巡逻兵数据;
追逐玩家时需要实现Start和Update函数;
该类继承自SSAction.
public class PatrolFollowAction : SSAction
{
private float speed = 2f; //跟随玩家的速度
private GameObject player; //玩家
private PatrolData data; //侦查兵数据
//更新
public override void Update()
{
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);
}
}
- 巡逻兵继续巡逻:GoPatrolAction.cs
巡逻兵巡逻动作让巡逻兵按照特点的轨迹运动巡逻;
需要变量记录巡逻兵的移动速度、移动距离、具体数据等;
需要实现Start和Update函数;
需要在Gopatrol函数中为巡逻兵规划路线。
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; //侦察兵的数据
public override void Update()
{
//侦察移动
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()
{
//根据当前的移动方向以及是否到达终点
//如果到达终点,选定下一个移动方向
//如果没到达终点,继续前进
}
}
- 调用巡逻兵动作:PatrolActionManager.cs/SSActionManager.cs
SSActionManager.cs重新实现了SSActionEvent函数,当intParam为0时巡逻兵追逐玩家,当intParam为1时巡逻兵继续巡逻。
public class SSActionManager : MonoBehaviour, ISSActionCallback
{
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();
}
}
}
PatrolActionManager调用SSActionManager(父类)的SSActionEvent函数实现巡逻兵运动的控制。
- 区域判断:AreaCollide.cs
区域判断是通过每个区域特定的标识来实现,通过对比区域的标识可以知道玩家所处的区域是否和巡逻兵相同。
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")
{
sceneController.wall_sign = sign;
}
}
}
3.游戏场景的逻辑:
游戏场景的逻辑使用了订阅与发布模式;
模式的优点在于降低代码的耦合度;
订阅者没必要知道和功能调用有关的类,只需要知道发布者即可;
而发布者调用对应的方法来进行实际操作。
GameEventManager作为发布者,FirstSceneController作为订阅者。
- 发布者:GameEvenManager.cs
该类专门发布事件,订阅者可以订阅相应的事件,然后由该类去施行相应的动作。
本游戏中发布者主要负责三个动作,分数变化、游戏结束、水晶数目。
public class GameEventManager : MonoBehaviour
{
//分数变化
public void PlayerEscape()
{
if (ScoreChange != null)
{
ScoreChange();
}
}
//游戏结束
public void PlayerGameover()
{
if (GameoverChange != null)
{
GameoverChange();
}
}
//水晶数量
public void ReduceCrystalNum()
{
if (CrystalChange != null)
{
CrystalChange();
}
}
}
- 订阅者:FirstSceneController.cs
该类向发布者请求事件。
FirstSceneController包含一些必要的变量记录游戏信息;
FirstSceneController需要实现载入资源函数;
FirstSceneController需要实现玩家移动函数;
FirstSceneController需要调用发布者提供的函数;
FirstSceneController需要控制游戏的结束。
public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController
{
public PropFactory patrol_factory; //巡逻兵工厂
public ScoreRecorder recorder; //计分器
public PatrolActionManager action_manager; //运动管理器
public int wall_sign = -1; //当前玩家所处哪个格子
public GameObject player; //玩家
public Camera main_camera; //主相机
public float player_speed = 5; //玩家移动速度
public float rotate_speed = 135f; //玩家旋转速度
private List<GameObject> patrols; //场景中巡逻者列表
private List<GameObject> crystals; //场景水晶列表
private bool game_over = false; //游戏结束标志
//资源载入函数,利用预设
public void LoadResources()
{
Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));
player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(0, 9, 0), Quaternion.identity) as GameObject;
crystals = patrol_factory.GetCrystal();
patrols = patrol_factory.GetPatrols();
//巡逻兵巡逻
for (int i = 0; i < patrols.Count; i++)
{
action_manager.GoPatrol(patrols[i]);
}
}
//玩家移动
public void MovePlayer(float translationX, float translationZ)
{
//如果游戏没有结束,进入玩家移动功能
//玩家使用动画进行移动和旋转
}
//控制游戏结束
void Gameover()
{
game_over = true;
patrol_factory.StopPatrol();
action_manager.DestroyAllAction();
}
}
4.UI界面的生成
UI界面的生成要使用UserGUI.cs来构造UI界面,就是一些画标签画按钮的工作。和上次作业唯一的区别就是改了改文字,所以不再赘述。
详细代码见gitee主页。
总结
这次作业主要学习了设计方式中的发布者订阅者模式,降低耦合度。
另一方面学习了人物模型和动画的使用。