小女孩与巡逻僵尸
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
subject:OnLostGoal
Publisher: ?
Subscriber: ? - 工厂模式生产巡逻兵
- 必须使用订阅与发布模式传消息
规则介绍
玩家使用ws或者↑↓控制角色前进/后退,使用ad或者←→控制角色向左向右转。控制角色避开僵尸的追击,每逃避一次就加一分,最高记录将一直显示。
视频演示
[Unity-3d]小女孩与鬼畜巡逻僵尸
订阅发布模式
本次作业要求使用订阅发布模式实现。简单来说,订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。该模式中发布者和订阅者没有直接的耦合,是实现模型与视图分离的重要手段。具体到这次的作业,将涉及到逃离僵尸追踪和被僵尸追上等事件需要监听,定义一个发布事件类,以及在场记类中实现订阅者,代码如下:
发布者
// 发布事件类 EventManager.cs 中的发布者
// 时间响应:分数变化
public delegate void scoreEvent();
public static event scoreEvent changeScore;
// 事件响应:游戏结束
public delegate void gameOverEvent();
public static event gameOverEvent gameOver;
// 事件1:逃离僵尸
public void PlayerEscape()
{
if (changeScore)
changeScore();
}
// 事件2:被僵尸追上
public void PlayerGameover()
{
if (gameOver)
gameOver();
}
订阅者
// 场记类 SceneController.cs 中的订阅者
public PatrolFactory patrolFactory; // 僵尸工厂
public ScoreRecorder recorder; // 记分牌
public PatrolActionManager actionManager; // 僵尸动作管理者
private bool isOver = false; // 游戏结束标识
void OnEnable() // 注册订阅
{
EventManager.changeScore += AddScore;
EventManager.gameOver += GameOver;
}
void OnDisable() //取消订阅
{
EventManager.changeScore -= AddScore;
EventManager.gameOver -= GameOver;
}
void AddScore() // 记分牌变化
{
recorder.addScore(); // 加分
}
void GameOver() // 游戏结束变化
{
isOver = true; // 游戏结束
patrolFactory.reset(); // 重置巡逻僵尸位置
actionManager.freezeAllAction(); // 停止所有僵尸动作
}
游戏具体部分实现
其实要实现的部分还算是比较多的,主要有巡逻僵尸部分、玩家角色部分、UI、记分牌等等,接下来将说一下核心的代码部分。
地图
首先当然是搭建地图啦,大家可以在AssetsStore上找到很多很好看的又免费的资源,但是为了节约时间,这一次我就弄了一个比较简单的九宫地图。
一个值得注意的问题就是如何防止僵尸们越界追人,于是我给每一个房间设了一个BoxCollider(如上图所示),这样当小女孩跟某一个BoxCollider有接触时,我们就可以知道她在哪个房间,就可以只允许对应房间僵尸运动了。
// ColliderBlock.cs
public int block = 0;
void OnTriggerEnter(Collider collider)
{
// 若小女孩进入该房间,更新p_block
if (collider.gameObject.tag == "Player")
{
sceneController.p_block = sign;
}
}
巡逻僵尸
首先,是把最基础的预制设置好,进入AssetsStore,本来是要搜索soilders的,结果在搜索结果里面发现了这么一套可爱的小人,还是免费的,果断下载。
OK,然后我们要做的是给巡逻僵尸加上Animator、CapsuleCollider、Rigidbody组件,分别是为了实现动画效果、添加碰撞体、添加刚体。除此之外,我们还要在他的子结点添加一个BoxCollider,并调整它的大小,这个碰撞体其实是实现一个探测器的效果,当小女孩与其碰撞时,就相当于小女孩进入僵尸的探测范围啦。
接下来就是巡逻僵尸的相关类实现啦。
巡逻僵尸基本属性类
// PatrolData.cs
public int block; // 巡逻兵当前区域
public int p_block = -1; // 小女孩当前区域
public bool isChasing = false; // 是否追踪小女孩
public GameObject player; // 玩家游戏对象
public Vector3 start_position; // 当前巡逻兵初始位置
巡逻僵尸工厂类
// PatrolFactory.cs
private GameObject temp = null; // 临时对象
private List<GameObject> patrols = new List<GameObject>(); // 巡逻僵尸队列
private Vector3[] pos = new Vector3[9]; // 巡逻僵尸们的初始位置
public List<GameObject> GetPatrols()
{
int[] pos_x = { -5, 5, 15 };
int[] pos_z = { -5, 5, 15 };
int index = 0;
// 设置初始位置
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
pos[index++] = new Vector3(pos_x[i], 0, pos_z[j]);
// 放置僵尸
for(int i = 0; i < 9; i++)
{
temp = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol2"));
temp.transform.position = pos[i];
temp.GetComponent<PatrolData>().sign = i + 1;
temp.GetComponent<PatrolData>().start_position = pos[i];
patrols.Add(temp);
}
return patrols;
}
public void reset() // 重置巡逻僵尸
{
for (int i = 0; i < used.Count; i++)
used[i].gameObject.GetComponent<Animator>().SetBool("run", false);
}
}
巡逻僵尸动作类
僵尸巡逻
// PatrolGo.cs 僵尸巡逻
private enum Direction { EAST, NORTH, WEST, SOUTH };
private float pos_x, pos_z; //移动前的初始x和z方向坐标
private float length; //移动的长度
private float p_speed = 1.2f; //巡逻移动速度
private bool isEnd = true; //是否到达路径终点
private Direction dir = Direction.EAST; //移动的方向
private PatrolData data; //巡逻僵尸的属性
public static PatrolGon getInstance(Vector3 loc, GameObject p)
{
PatrolGo instance = CreateInstance<PatrolGo>();
instance.pos_x = loc.x;
instance.pos_z = loc.z;
instance.length = Random.Range(4, 7);
return instance;
}
public override void Start()
{
this.gameobject.GetComponent<Animator>().SetBool("run", true);
data = this.gameobject.GetComponent<PatrolData>();
}
public override void Update()
{
if (isEnd)
{
switch (dir)
{
case Direction.EAST:
pos_x += length;
break;
case Direction.WEST:
pos_x -= length;
break;
case Direction.NORTH:
pos_z += length;
break;
case Direction.SOUTH:
pos_z -= length;
break;
}
isEnd = false;
}
Vector3 des = new Vector3(pos_x, 0, pos_z);
this.transform.LookAt(des);
if (Vector3.Distance(this.transform.position, des) > 0)
this.transform.position =
Vector3.MoveTowards(this.transform.position, des, p_speed * Time.deltaTime);
else {
dir++;
if (dir > Direction.SOUTH)
dir = Direction.EAST;
isEnd = true;
}
//若僵尸探测到玩家,巡逻动作结束,开始追踪
if (data.follow_player && data.wall_sign == data.sign)
{
this.destroy = true;
this.callback.SSActionEvent(this,0,this.gameobject);
}
}
僵尸追踪
// PatrolChase.cs 僵尸追踪
private float speed = 2f; // 追踪玩家的速度
private GameObject player; // 玩家
private PatrolData data; // 巡逻僵尸属性
public static PatrolFollowAction GetSSAction(GameObject player)
{
PatrolFollowAction action = CreateInstance<PatrolFollowAction>();
action.player = player;
return action;
}
public override void Start()
{
data = this.gameobject.GetComponent<PatrolData>();
}
public override void Update()
{
this.transform.LookAt(player.transform.position);
this.transform.position =
Vector3.MoveTowards(this.transform.position, player.transform.position, c_speed * Time.deltaTime);
//若僵尸探测不到玩家,追踪动作结束,开始巡逻
if (!data.follow_player || data.wall_sign != data.sign)
{
this.destroy = true;
this.callback.SSActionEvent(this,1,this.gameobject);
}
}
以上动作类实现完之后,即可在动作管理类中调用。如在结束巡逻并使用回调函数后追踪玩家,又或者是丢失玩家并使用回调函数重新巡逻。
小女孩
相对僵尸巡逻和追踪两种行动模式来说,小女孩的操作就简单多了,只需要通过GUI让玩家控制小女孩的行动就好。
UI类
// UserInterface.cs
private IUserAction action;
void Update()
{
// 键入方向键,移动小女孩
float change_x = Input.GetAxis("Horizontal");
float change_z = Input.GetAxis("Vertical");
action.MovePlayer(change_x, change_z);
}
场景控制类
// SceneController.cs
//玩家移动
public void MovePlayer(float change_x, float change_z)
{
if(!game_over)
{
// 若在走动则设置run动画,否则静止
if (change_x != 0 || change_z != 0)
player.GetComponent<Animator>().SetBool("run", true);
else
player.GetComponent<Animator>().SetBool("run", false);
// 移动小女孩
player.transform.Translate(0, 0, change_z * player_speed * Time.deltaTime);
player.transform.Rotate(0, change_x * 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);
}
}
各种交互事件
僵尸的探测器 与 小女孩 交互
// ColliderDetector.cs
void ChasingFlag(Collider girl)
{
if (girl.gameObject.tag == "Player")
{
// 小女孩被探测到
this.gameObject.transform.parent.GetComponent<PatrolData>().isChasing = true;
this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
}
}
void EscapeFlag(Collider collider)
{
if (collider.gameObject.tag == "Player")
{
// 小女孩脱离探测范围
this.gameObject.transform.parent.GetComponent<PatrolData>().isChasing = false;
this.gameObject.transform.parent.GetComponent<PatrolData>().player = null;
}
}
僵尸 与 小女孩 交互
// ColliderPatrol.cs
void collide(Collision collider)
{
if (collider.gameObject.tag == "Player")
{
// 小女孩与僵尸相碰撞
colliderr.gameObject.GetComponent<Animator>().SetTrigger("death");
Singleton<GameEventManager>.Instance.PlayerGameover();
}
}
小结
以上便是基本的核心代码了,还有一些记分牌、摄像头跟踪什么的,这里不做赘述,都是一些可以随意发挥的东西。本次作业就写到这里了,特别鸣谢C486C大神的博客,在下实在是佩服的五体投地hhh。