3D 游戏与编程 Homework 6
实验内容
- 智能巡逻兵
- 提交要求:
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
- subject:OnLostGoal
- Publisher: ?
- Subscriber: ?
- 工厂模式生产巡逻兵
- 必须使用订阅与发布模式传消息
- 友善提示1:生成 3~5个边的凸多边型
- 随机生成矩形
- 在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
- 5 ?
- 友善提示2:参考以前博客,给出自己新玩法
实验环境
- Windows
- Unity 2020.3.18
技术日记
一、模型与动画
1. 模型与动画基础知识
- 模型(Model)
- 物体对象的组合,Unity 映射为游戏对象树
- 每个物体包含一个网格(Mesh)与蒙皮设置
- 包含使用的纹理、材质以及一个或多个动画剪辑
- 模型由 3D 设计人员使用 3D Max 等创建
- 动画剪辑(Animation Clip)
- 物体或骨骼的位置等属性在关键帧的时间序列
- 通过插值算法计算每帧的物体的位置或属性
2. Mecanim动画状态机(CrowAnimatorController)
设计状态机控制器包括 状态、变迁、参数、条件 四个部分设计。
观察状态的属性:
- Motion 动画剪辑
- Speed 速度(倍率)
- Foot IK?
- …
- Transtions
- 默认:按顺序检测生效转移
- Solo:优先检测转移
- Mute:禁止转移
- 具有质量
- 具有中心
- 具有质心(不考虑形状)
- 假设1:物体是均质的
- 假设2:中心与质心重合
- 物体作用力分解为:
- 作用中心点上的力(Force)
- 围绕中心点旋转的力矩(Torque)
设置变迁:
- 注意变迁的顺序
- 设定 solo 或 mute
- 设定每个变迁
- 给变迁命名,便于控制
- 是否使用动画结束条件
- 变迁 动画混合(平滑过程)
- 变迁条件,例如:
- Fly->exit 条件是 live
- Not live 从任意状态转死亡
控制变迁条件:
- 使用转移控制变量
- Float,Int,Bool 类型
- Trigger 类型
- 规划转移变量与变迁事件发生条件
- 转移变量设计
- 建议多使用 trigger 类型变量
- 确保转移条件唯一,避免使用顺序决定转移(位操作通常OK)
- 使用 mute 关闭不用的转移
- 转移变量设计
- 条件设计,例如:
- live = false 转入死亡
- fly_attack_trigger 确定转入攻击
3. 类人动画与动画中级知识
Avatar的意思化身(印度教和佛教中化作人形或兽形的神)。不用说兽幻化成人形,即使是植物,草也会有点头这样的拟人动作。因此Unity 的动画系统具有处理人形角色的特殊功能。因为人形角色在游戏中很常见,所以 Unity 为人形动画提供专门的工作流程以及扩展工具集。
- 类人骨架是非常常用的特例,并且在游戏中广泛使用
- 人骨骼结构的类似性,使得把动画从一个类人骨架映射到另一个成为可能。
- 只要模型的子对象名字一样
- 如果名字不一样,就需要映射
二、实验部分
1.项目配置过程
新建3d-unity的文件,然后直接把gitee上Assets
文件夹替换新项目的Assets
文件夹,同时,把scenes中的myScene拖出来,直接点击运行即可开始游戏。
2. 实现思路及核心算法
1)PatrolAction.cs
定义了巡逻兵的行为及运动:
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
主要函数及代码分析如下:
//定义巡逻的方向
private enum Dirction { EAST, NORTH, WEST, SOUTH };
//定义巡逻兵的位置
private float pos_x, pos_z;
//定义巡逻兵的移动的距离
private float move_length;
//定义巡逻兵的移动的速度
private float move_speed = 1.2f;
//定义巡逻兵是否移动
private bool move_sign = true;
//定义巡逻兵的初始移动方向
private Dirction dirction = Dirction.EAST;
private PatrolData data;
//通过move_sign判断巡逻兵是否能够移动,如果能移动则判断是否到达正方形的四个角之一(碰到障碍物),如果到达了,则以自身为原点位置,重新计算下一步的移动位置。
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;
}
}
2) PatrolFollowAction.cs
定义了巡逻兵的跟踪:
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
void Follow(){
//巡逻兵朝玩家的方向移动
transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime);
this.transform.LookAt(player.transform.position);
}
3)PatrolFollowAction.cs
定义了巡逻兵的碰撞——玩家:
- 巡逻兵若追击到玩家,则会把玩家杀死,游戏则结束;
public class PlayerCollideDetection : MonoBehaviour {
void OnCollisionEnter(Collision other){
if (other.gameObject.tag == "Player")
{
other.gameObject.GetComponent<Animator>().SetBool("death",true);
this.GetComponent<Animator>().SetTrigger("shoot");
Singleton<GameEventManager>.Instance.PlayerGameover();
}
}
}
此处调用了Animator的状态机,设置状态机为“death”状态,触发器为“shoot”。
4)PlayerInDetection.cs
定义了巡逻兵与玩家之间的追逐:
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
public class PlayerInDetection : MonoBehaviour{
void OnTriggerEnter(Collider collider){
//玩家进入巡逻兵范围
if (collider.gameObject.tag == "Player"){
this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = true;
this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
//巡逻兵追踪玩家
this.gameObject.transform.parent.GetComponent<Animator>().SetTrigger("shock");
}
}
void OnTriggerExit(Collider collider){
//玩家离开巡逻兵范围,停止追踪
if (collider.gameObject.tag == "Player"){
this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = false;
this.gameObject.transform.parent.GetComponent<PatrolData>().player = null;
}
}
}
5)FirstSceneController.cs
加载预设资源:巡逻兵、记分板、玩家、地图等
public void LoadResources(){
//加载玩家
player = _PatorlFactory.LoadPlayer();
//加载巡逻兵
patrols = _PatorlFactory.LoadPatrol();
for (int i = 0; i < patrols.Count; i++){
//绑定巡逻兵巡逻的动作
_PatrolActionManager.GoPatrol(patrols[i]);
}
//加载地图
GameObject.Instantiate(Resources.Load("Prefabs/Plane"), new Vector3(0, 0, 0), Quaternion.identity);
}
发布与订阅模式:
- 传递分数的变化和游戏状态的变化
//发布与订阅模式
void OnEnable()
{
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameoverChange += Gameover;
}
void OnDisable()
{
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameoverChange -= Gameover;
}
玩家的移动:
- 游戏未结束,则可以控制玩家。
public void MovePlayer(float translationX, float translationZ){
if(!game_over){
if (translationX != 0 || translationZ != 0){
//run动画
player.GetComponent<Animator>().SetBool("run", true);
}
else
{
player.GetComponent<Animator>().SetBool("run", false);
}
//通过键盘输入移动玩家。
player.transform.Translate(translationX * player_speed * Time.deltaTime, 0, translationZ * player_speed * Time.deltaTime);
if (player.transform.position.y != 0){
player.transform.position = new Vector3(player.transform.position.x, 0, player.transform.position.z);
}
}
}
通过UserGUI.cs
中读取键盘输入的translationX
和translationZ
控制玩家行走。
6)GameEventManager.cs
控制主要的游戏事件。
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();
}
}
}
PatrolData.cs
巡逻兵的信息。
public class PatrolData : MonoBehaviour{
public int sign;
public bool follow_player = false;
public int wall_sign = -1;
public GameObject player;
public Vector3 start_position;
}
7)UserGUI.cs
游戏界面及键盘输入:
void Update(){
//读取键盘输入
float translationX = Input.GetAxis("Horizontal");
float translationZ = Input.GetAxis("Vertical");
action.MovePlayer(translationX, translationZ);
}
private void OnGUI(){
GUI.Label(new Rect(10, 5, 200, 50), "Score:", style);
GUI.Label(new Rect(70, 5, 200, 50), action.GetScore().ToString(), style);
if(action.GetScore() == 10){
GUI.Label(new Rect(Screen.width / 2 - 30, Screen.width / 2 - 220, 100, 100), "Pass!", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "Restart")){
action.Restart();
return;
}
}
else if(action.GetGameover() && action.GetScore() != 20){
GUI.Label(new Rect(Screen.width / 2 - 70, Screen.width / 2 - 220, 100, 100), "Game Over!", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "Restart")){
action.Restart();
return;
}
}
}
8)PatorlFactory.cs
,PatorlActionManager.cs
,ScoreRecorder.cs
,SSDirector.cs
,SSAction.cs
,SSActionManager.cs
与前几次作业的差别不大,不再重复分析。
9)接口类:ISenceInterface.cs
,ISSActionInterface.cs
,IUserInterface.cs
其中,用户接口IUserInterface.cs
,提供用户能直接调用的接口:MovePlayer(), GetScore(), GetGameover(), Restart()。
三、总结
参考:https://blog.youkuaiyun.com/C486C/article/details/80153548