游戏设计:
• 创建一个地图和若干巡逻兵(使用动画);
• 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
• 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
• 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
• 失去玩家目标后,继续巡逻;
• 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计:
• 使用订阅与发布模式传消息
• 工厂模式生产巡逻兵
游戏设计
基于上面的几点,我们设计这样一个游戏场景:
1.我们的地图有3×3共计9块区域,不同区域间有部分连通,每个区域有一个巡逻兵。
2.地图上会随机生成一些任务点,作为玩家需要到达的目标位置,这些任务点需要玩家全部到达且未被巡逻兵碰撞到方为获胜。
3.每到达一个任务点时为甩掉巡逻兵一次,计一分。
4.每次游戏任务点位置随机分布在地图上,但总的任务点数量一定。
5.玩家初始位置为地图区域的中心位置。
游戏展示
游戏操作演示1-游戏各项操作(11.7M)
游戏操作演示2-失败(1.17M)
游戏操作演示3-完成(0.9M)
游戏预制制作
基于上面的要求与设计,可以先为我们的游戏场景创建4个预制:
地图、玩家、“任务点”、巡逻兵。
地图我使用了Low Poly v2资源包进行预制制作。
玩家和巡逻兵我基于SoldierFree资源包中的预制制作,我们为两个预制添加Rigidbody和Capsule Collider来实现玩家与巡逻兵的碰撞。
对于巡逻兵的预制,我们添加了两个脚本,一个脚本用于记录巡逻兵对象的一些信息,另一个脚本用于处理玩家与巡逻兵相撞后的情况。另外我们添加了一块方形的碰撞器(设置为触发器)来探测玩家,并添加了相应的脚本来使得有玩家进入/离开该方形区域时,巡逻兵开始/停止追捕玩家。
我们使用了Animator组件,通过控制器来使得对应的对象做出相应的动画行为。它是使用状态机来管理运动的,我们在脚本中可以设置状态的改变,从而对应有对象(玩家或巡逻兵)的动画表现。然后我们将玩家对象和巡逻兵对象对应的控制器加到预制上。
“任务点”目前是简单的用红色圆球来代替(其碰撞体积也为圆球)。
这样我们就拥有了一些游戏的基本元素。
脚本编写
我们的实现要求是:订阅与发布模式传消息和工厂模式生产巡逻兵。
我们先给出主摄像机上附加脚本CameraMove
,从而我们可以在移动玩家游戏对象时伴随镜头的移动。
public class CameraMove : MonoBehaviour
{
public GameObject follow; // 跟随的对象
public float speed = 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, speed * Time.deltaTime); // 摄像机自身位置到目标位置平滑过渡
}
}
对于我们的巡逻兵,我们有一个脚本PatrolData
附加其上,用于保存它的一些信息
public class PatrolData : MonoBehaviour
{
public int block; // 巡逻兵所在区域编号
public bool follow_player = false; // 是否正在追逐玩家
public int player_block = -1; // 此时玩家所在区域编号
public GameObject player; // 玩家游戏对象
public Vector3 start_position; // 巡逻兵初始位置
}
接着我们给出几个预制中与碰撞相关的脚本(里面使用了控制器等部分后续给出)。
首先对于场景,我们对9块区域都附加上PlaneCollide
脚本,以更新玩家所在的区域信息。
public class PlaneCollide : MonoBehaviour
{
public int block = 0;
FirstSceneController sceneController;
private void Start()
{
sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
}
void OnTriggerEnter(Collider collider)
{
// 标记玩家进入区域
if (collider.gameObject.tag == "Player")
{
sceneController.player_block = block;
}
}
}
然后对于巡逻兵,由于游戏失败的条件是由巡逻兵与玩家相撞,故我们附加脚本PatrolPlayerCollide
处理这一情况:
public class PatrolPlayerCollide : MonoBehaviour
{
void OnCollisionEnter(Collision other)
{
if (other.gameObject.tag == "Player") // 当巡逻兵与玩家相撞
{
other.gameObject.GetComponent<Animator>().SetTrigger("death"); // 改变玩家的状态
this.GetComponent<Animator>().SetTrigger("shoot"); // 改变巡逻兵的状态
Singleton<GameEventManager>.Instance.PlayerGameover(); // 游戏结束
}
}
}
巡逻兵还需要对其附近的区域进行探测,当玩家进入其探测区域,巡逻兵会对玩家进行追逐,故我们有脚本PatrolCollide
:
public class PatrolCollide : 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;
}
}
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;
}
}
}
对于碰撞,我们还有一种情况需要处理——当玩家与我们的“任务点”碰撞,“任务点”消失,需要完成的任务数减少。我们有脚本TaskCollide
:
public class TaskCollide : MonoBehaviour
{
void OnTriggerEnter(Collider collider)
{
if (collider.gameObject.tag == "Player" && this.gameObject.activeSelf)
{
this.gameObject.SetActive(false);
Singleton<GameEventManager>.Instance.ReduceTaskNum(); // 减少任务数量
}
}
}
然后我们运用工厂模式生产巡逻兵(和“任务点”),我们有工厂类Factory
。可以看到我们巡逻兵的出生点是固定的(玩家出生点也是固定的,避免二者开局就相遇),而任务点是在地图中随机刷新的。
public class Factory : MonoBehaviour
{
private GameObject patrol = null; // 巡逻兵
private List<GameObject> used = new List<GameObject>(); // 正在被使用的巡逻兵
private GameObject task = null; // 任务点
private List<GameObject> usedtask = 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 };
for(int i=0;i < 3;i++) // 生成不同的巡逻兵初始位置
{
for(int j=0;j < 3;j++)
{
vec[i*3+j] = new Vector3(pos_x[i], 0, pos_z[j]);
}
}
for(int i=0; i < 9; i++) // 依次生成放置巡逻兵
{
patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol"));
patrol.transform.position = vec[i];
patrol.GetComponent<PatrolData>().block = i + 1;
patrol.GetComponent<PatrolData>().start_position = vec[i];
used.Add(patrol);
}
return used;
}
public List<GameObject> GetTask() // 生成任务点
{
for(int i=0;i<12;i++)
{
task = Instantiate(Resources.Load<GameObject>("Prefabs/Task"));
float ranx = Random.Range(-range, range);
float ranz = Random.Range(-range, range);
task.transform.position = new Vector3(ranx, 0, ranz);
usedtask.Add(task);
}
return usedtask;
}
public void StopPatrol() // 游戏结束,使所有巡逻兵停止运动
{
for (int i = 0; i < used.Count; i++)
{
used[i].gameObject.GetComponent<Animator>().SetBool("run", false); // 切换状态
}
}
}
这样我们就定义了工厂类来生成巡逻兵,我们游戏最核心的控制部分为SceneController
,这个控制器里我们就调用它来生成了巡逻兵。对于该控制器,我们应该让其最先运行,所以我们要在项目设置-脚本执行顺序中加入它,并把它放在较前的位置,如下图:
该控制器串联起了游戏的各个部分,其内容如下:
using UnityEngine.SceneManagement;
public class SceneController : MonoBehaviour, IUserAction, ISceneController
{
public Factory patrol_factory; // 巡逻兵工厂
public ScoreRecorder recorder; // 记录得分
public PatrolActionManager action_manager; // 运动管理器
public int player_block = -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> tasks; // 地图中任务列表
private bool game_over = false; // 游戏结束状态
void Update()
{
for (int i = 0; i < patrols.Count; i++) // 更新所有巡逻兵掌握的玩家区位信息
{
patrols[i].gameObject.GetComponent<PatrolData>().player_block = player_block;
}
if(recorder.GetTaskNumber() == 0) // 任务全部完成
{
Gameover();
}
}
void Start()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
patrol_factory = Singleton<Factory>.Instance;
action_manager = gameObject.AddComponent<PatrolActionManager>() as PatrolActionManager;
LoadResources();
main_camera.GetComponent<CameraMove>().follow = player;
recorder = Singleton<ScoreRecorder>.Instance;
}
public void LoadResources()
{
Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));
player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(0, 9, 0), Quaternion.identity) as GameObject;
tasks = patrol_factory.GetTask(); // 生成任务点
patrols = patrol_factory.GetPatrols(); // 生成巡逻兵
for (int i = 0; i < patrols.Count; i++) // 使所有巡逻兵移动
{
action_manager.GoPatrol(patrols[i]);
}
}
public void MovePlayer(float translationX, float translationZ) // 玩家移动
{
if(!game_over)
{
if (translationX != 0 || translationZ != 0)
{
player.GetComponent<Animator>().SetBool("run", true);
}
else
{
player.GetComponent<Animator>().SetBool("run", false);
}
//移动和旋转
player.transform.Translate(0, 0, translationZ * player_speed * Time.deltaTime);
player.transform.Rotate(0, translationX * rotate_speed * Time.deltaTime, 0);
//防止碰撞带来的移动
if (player.transform.localEulerAngles.x != 0 || player.transform.localEulerAngles.z != 0)
{
player.transform.localEulerAngles = new Vector3(0, player.transform.localEulerAngles.y, 0);
}
if (player.transform.position.y != 0)
{
player.transform.position = new Vector3(player.transform.position.x, 0, player.transform.position.z);
}
}
}
public int GetScore()
{
return recorder.GetScore();
}
public int GetTaskNumber()
{
return recorder.GetTaskNumber();
}
public bool GetGameover()
{
return game_over;
}
public void Restart()
{
SceneManager.LoadScene("Scenes/mySence");
}
void OnEnable()
{
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameoverChange += Gameover;
GameEventManager.TaskChange += ReduceTaskNumber;
}
void OnDisable()
{
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameoverChange -= Gameover;
GameEventManager.TaskChange -= ReduceTaskNumber;
}
void ReduceTaskNumber()
{
recorder.ReduceTask();
}
void AddScore()
{
recorder.AddScore();
}
void Gameover()
{
game_over = true;
patrol_factory.StopPatrol();
action_manager.DestroyAllAction();
}
}
在我们的场景控制器中,它作为订阅者,订阅了GameEventManager
中的事件。有对应的事件发生,场景控制器就会调用相应的方法,GameEventManager
部分如下:
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 TaskEvent(); // 剩余任务数量变化
public static event TaskEvent TaskChange;
public void PlayerEscape() // 玩家逃脱追捕
{
if (ScoreChange != null)
{
ScoreChange();
}
}
public void PlayerGameover() // 玩家被追到
{
if (GameoverChange != null)
{
GameoverChange();
}
}
public void ReduceTaskNum() // 剩余任务数量减少
{
if (TaskChange != null)
{
TaskChange();
}
}
}
而像我们上面给出的TaskCollide
、PatrolPlayerCollide
中,也有消息的发布,它们发布的消息都会被“订阅者”场景控制器接收到做相应的处理。
除此之外,我们还有在玩家逃脱巡逻兵追捕时,也会发布消息,使得场景控制器变更得分。这部分涉及到巡逻兵的移动动作——
巡逻兵主要在干两件事情:一是巡逻,这时玩家在其探测区域之外(或者并不在该巡逻兵所在的区块);二是追逐。
首先我们有SSAction
类作为游戏的动作基类。相应的,我们有动作管理器SSActionManager
。我们里面有向订阅者“通报消息”的部分,因此运动状态的变化会影响场景。我们还有游戏的总导演SSDirector
。
基类的代码我们不再给出,我们给出巡逻兵的运动的实现,这包括三部分:
1.巡逻部分的脚本PatrolGoAction
:
public class PatrolGoAction : 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 PatrolGoAction() { }
public static PatrolGoAction GetSSAction(Vector3 location)
{
PatrolGoAction action = CreateInstance<PatrolGoAction>();
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.player_block == data.block) // 如果巡逻兵需要跟随玩家,且玩家在巡逻兵所在区域,停止巡查开始追捕
{
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;
}
}
}
2.追逐部分的脚本PatrolFollowAction
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.player_block != data.block) // 如果巡逻兵无需跟随玩家/玩家不再巡逻兵所在区域,停止追捕开始巡逻
{
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);
}
}
3.巡逻兵的动作管理器PatrolActionManager
public class PatrolActionManager : SSActionManager
{
private PatrolGoAction go_patrol; // 巡逻兵巡逻
public void GoPatrol(GameObject patrol)
{
go_patrol = PatrolGoAction.GetSSAction(patrol.transform.position);
this.RunAction(patrol, go_patrol, this);
}
public void DestroyAllAction() // 停止所有动作
{
DestroyAll();
}
}
最后是我们游戏的图形化界面UserGUI
部分,该部分向玩家提供了大量的游戏信息:
public class UserGUI : MonoBehaviour {
private IUserAction action;
private GUIStyle score_style = new GUIStyle();
private GUIStyle text_style = new GUIStyle();
private GUIStyle over_style = new GUIStyle();
public int game_time = 0;
void Start ()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
text_style.normal.textColor = new Color(0, 0, 0, 1);
text_style.fontSize = 16;
score_style.normal.textColor = new Color(1,0.92f,0.016f,1);
score_style.fontSize = 16;
over_style.fontSize = 25;
StartCoroutine(GameTimer());
}
void Update()
{
//获取方向键的偏移量
float translationX = Input.GetAxis("Horizontal");
float translationZ = Input.GetAxis("Vertical");
//移动玩家
action.MovePlayer(translationX, translationZ);
}
private void OnGUI()
{
GUI.Label(new Rect(Screen.width - 170, 10, 50, 50), "剩余任务数:", text_style);
GUI.Label(new Rect(Screen.width - 80, 10, 50, 50), action.GetTaskNumber().ToString(), score_style);
GUI.Label(new Rect(Screen.width - 170, 30, 50, 50), "当前得分:", text_style);
GUI.Label(new Rect(Screen.width - 80, 30, 50, 50), action.GetScore().ToString(), score_style);
GUI.Label(new Rect(Screen.width - 170, 50, 50, 50), "当前用时:", text_style);
GUI.Label(new Rect(Screen.width - 80, 50, 50, 50), game_time.ToString(), score_style);
GUI.Label(new Rect(20 ,10, 100, 100), "游戏提示", text_style);
GUI.Label(new Rect(20, 30, 100, 100), "按WSAD或方向键移动", text_style);
GUI.Label(new Rect(20, 50, 100, 100), "成功躲避巡逻兵追捕加1分到达一个任务点", text_style);
GUI.Label(new Rect(20, 70, 100, 100), "你需要到达所有的任务点", text_style);
if(action.GetGameover() && action.GetTaskNumber() != 0)
{
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.width / 2 - 250, 100, 100), "遗憾,任务失败。", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "再来一局"))
{
action.Restart();
return;
}
}
else if(action.GetTaskNumber() == 0)
{
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.width / 2 - 250, 100, 100), "恭喜,完成任务!", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "再来一局"))
{
action.Restart();
return;
}
}
}
public IEnumerator GameTimer()
{
while (!action.GetGameover())
{
yield return new WaitForSeconds(1);
game_time++;
}
}
}
总之,在上面的实现中,我们运用了发布与订阅模式(它可以定义了一种一对多的依赖关系,实现了让多个订阅者对象同时监听某一个主题对象。当监听到变化时,订阅者即可据此做出相应的变化。)和工厂模式(批量生成了巡逻兵)。
本人的“3D游戏编程与设计”系列合集,请访问:
https://www.yizuodi.cn/category/3DGame/
Comments | NOTHING
<根据相关法律法规要求,您的评论将在审核后展示。