巡逻兵游戏
要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束
未完成要求:
- 使用动画实现巡逻兵行走,追捕以及玩家行走,死亡的动作;
- 使用工厂模式生产巡逻兵;
Bug:
- Restart后的游戏场景颜色大变,感觉是重新加载场景中使用的函数加载的场景问题;
- 游戏模式要求玩家严格从一个门进,从另一个门出,也就是说,存在无限从一个门进入刷分的可能性。这应该设计当玩家离开地图范围时游戏自动结束,只有当玩家在地图中时游戏状态才处于“游戏中”。
设计思路
本次实验大致分为以下几个部分:
游戏对象预设
游戏对象预设这部分需要完成巡逻兵(Cube)、玩家(Cube)和房间(Maze)的设计。
房间(Maze)
如图,房间底部由三个正方形拼接而成,墙则是由Cube凭借而成。材质图片在网上下载导入即可。
房间需要获取当前房间内的巡逻兵(Patrol),以便改变其当前运动状态。
//获取当前房间内的patrol
public class CurrentPatrol : MonoBehaviour {
public GameObject patrol;
void OnCollisionEnter(Collision other)
{
if (other.gameObject.tag == "patrol") {
patrol = other.gameObject;
}
}
}
在地图的边界有碰撞体Gate,用于检测玩家进入或离开该房间。Gate在碰撞发生时发布消息,由计分器(ScoreController)完成分数++的操作。
//使用订阅与发布模式传递消息
public class GateController : MonoBehaviour {
public delegate void AddScore ();
public static event AddScore addScore;
void OnTriggerEnter(Collider other)
{
//碰撞时检测,改变patrol状态,发布加分的事件
if (other.gameObject.tag == "player") {
if (this.transform.parent.GetComponent<CurrentPatrol> ().patrol.GetComponent<PatrolController> ().state == 0)
this.transform.parent.GetComponent<CurrentPatrol> ().patrol.GetComponent<PatrolController> ().state = 1;
else {
this.transform.parent.GetComponent<CurrentPatrol> ().patrol.GetComponent<PatrolController> ().state = 0;
if (addScore != null)
addScore ();
}
}
玩家(Cube)
外形为红色正方体。
玩家部分检测与巡逻兵的碰撞。如碰撞,发布游戏结束的消息,由场记(SceneController)处理。
public class PlayerController : MonoBehaviour {
public delegate void GameOver ();
public static event GameOver gameOver_message;
void OnCollisionEnter(Collision other)
{
//检测碰撞,如碰撞,发布游戏结束消息
if (other.gameObject.tag == "patrol") {
if (gameOver_message != null)
gameOver_message ();
}
this.GetComponent<Rigidbody> ().velocity = new Vector3 (0, 0, 0);
}
void Start()
{
//取消碰撞后的旋转
this.GetComponent<Rigidbody> ().freezeRotation = true;
}
}
巡逻兵(Cube)
滑稽正方体。
巡逻兵需要完成:
- 没有玩家进入时循环巡逻(patrol());
- 有玩家进入时追捕玩家(chase(player))。
public class PatrolController : MonoBehaviour {
private float x, z; //记录巡逻兵位置
public float speed = 0.5f; //巡逻兵速度
public int state = 0; //巡逻兵状态,0为巡逻,1为追捕
public GameObject player; //追捕对象(玩家)
int turn = 0;
private bool flag = true;
private float dis = 0;
public SceneController currentsceneController;
// Use this for initialization
void Start () {
currentsceneController = Director.get_instance ().current_scene;
player = currentsceneController.player;
x = this.transform.position.x;
z = this.transform.position.z;
}
void FixedUpdate()
{
if (state == 0) {
patrol ();
} else if (state == 1) {
chase (player);
}
}
void patrol()
{
//六边形巡逻
if (flag) {
switch (turn) {
case 0:
x++;
z--;
break;
case 1:
x = x + 2;
break;
case 2:
x++;
z++;
break;
case 3:
x--;
z++;
break;
case 4:
x = x - 2;
break;
case 5:
x--;
z--;
break;
}
flag = false;
}
dis = Vector3.Distance (this.transform.position, new Vector3 (x, 1f, z));
//判断是否到达指定位置(由于浮点数没有相等,则当dis小于某个值时判定到达)
if (dis > 0.7) {
transform.position = Vector3.MoveTowards (this.transform.position, new Vector3 (x, 1f, z), speed * Time.deltaTime);
} else {
turn = (turn + 1) % 6;
flag = true;
}
}
void chase(GameObject other)
{
//追捕,朝玩家位置移动
transform.position = Vector3.MoveTowards (this.transform.position, player.transform.position, 1.5f * speed * Time.deltaTime);
}
}
游戏控制
控制部分包括计分器(ScoreController),导演(Director),场记(SceneController)。
计分器(ScoreController)
计分器订阅Gate发布的分数增加事件并处理。
public class ScoreController : MonoBehaviour {
public int Score = 0;
// Use this for initialization
void OnEnable(){
GateController.addScore += add;
}
void OnDisable(){
GateController.addScore -= add;
}
void add(){
Score++;
}
void Start ()
{
//设置当前场景的计分器
Director.get_instance ().current_score = this;
}
}
导演(Director)
与之前几次实验大致相同,采用单例模式。
public class Director : System.Object{
private static Director instance;
public SceneController current_scene; //当前场景
public ScoreController current_score; //当前计分器
public static Director get_instance(){
if (instance == null) {
instance = new Director ();
}
return instance;
}
}
场记(SceneController)
场记需要实现两个接口。
public interface scene_interface
{
void LoadResources(); //加载资源
}
public interface UserAction
{
void Restart(); //重启
}
除此之外,场记需要订阅玩家发布的游戏结束消息并进行处理。
//加载资源,这里偷懒了就没用工厂模式了。。
//加载地图(5个房间拼接),每个房间加载一个巡逻兵
public void LoadResources ()
{
player = Instantiate (Resources.Load ("Prefabs/Player")) as GameObject;
int x = 0, z = 0;
int temp = 1;
Instantiate (Resources.Load ("Prefabs/maze"), new Vector3 (x, 0, z), Quaternion.identity);
Instantiate (Resources.Load ("Prefabs/Patrol"), new Vector3 (x - 3, 1.5f, z), Quaternion.identity);
for (int i = 0; i < 2; i++) {
for (int k = 0; k < 2; k++) {
temp = -temp;
x = 15 * i * temp;
z = (2 - i) * 5 * temp;
Instantiate (Resources.Load ("Prefabs/maze"), new Vector3 (x, 0, z), Quaternion.identity);
Instantiate (Resources.Load ("Prefabs/Patrol"), new Vector3 (x - 3, 1.5f, z), Quaternion.identity);
}
}
}
//对玩家发布的游戏结束消息的处理
void OnEnable()
{
PlayerController.gameOver_message += GameOverEvent;
}
void OnDisable()
{
PlayerController.gameOver_message -= GameOverEvent;
}
void GameOverEvent()
{
result = "Game Over!!!";
game_state = 0;
}
//重新加载场景(感觉这个函数有点问题。。Restart之后场景颜色大变,有点难受)
public void Restart()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
game_state = 1;
}
UI及其他
这部分包括UI设计,以及增加相机随玩家移动而移动。
UI设计
UI需要完成使用方向键操控玩家移动,并展示游戏规则,游戏结束标志以及当前分数。
//方向键控制移动
void Update () {
if (currentscene.game_state != 0) {
float translationX = Input.GetAxis ("Horizontal") * speed * Time.deltaTime;
float translationZ = Input.GetAxis ("Vertical") * speed * Time.deltaTime;
player.transform.Translate (translationX, 0, 0);
player.transform.Translate (0, 0, translationZ);
}
}
void OnGUI()
{
//字体样式
GUIStyle font = new GUIStyle ();
font.fontSize = 30;
font.normal.textColor = new Color (255, 255, 255);
if (currentscene.game_state == 0) {
GUI.Label (new Rect (220, 80, 120, 40), currentscene.result, font); // 游戏结束标志
} else {
if (GUI.RepeatButton (new Rect (0, 0, 120, 40), "Rule")) {
GUI.Label (new Rect(220, 50, 500, 500), "Use direction key to move your ball.\nTry you best to avoid the chaser to \nget one point. If you are caught, game over.", font);
} //游戏规则
//当前分数
GUI.Label (new Rect (0, 120, 120, 40), "Score: " + Director.get_instance ().current_score.Score, font); //
}
//重启按钮
if (GUI.Button (new Rect (0, 60, 120, 40), "Restart"))
ui.Restart ();
}
相机移动
public class CameraMove : MonoBehaviour {
public GameObject player;
private SceneController currentscene;
private Vector3 offset;
// Use this for initialization
void Start () {
currentscene = Director.get_instance ().current_scene;
player = currentscene.player;
offset = player.transform.position - this.transform.position;
}
// Update is called once per frame
void Update () {
this.transform.position = player.transform.position - offset;
}
}
总结
做完实验对发布与订阅模式有点思路了,之前感觉有点迷。动画资源还是个没掌握的点,之后要抽时间自己学习一下,再把这个巡逻兵的游戏改进一下。现有做出来的东西仍然摆脱不了“用户体验极差“的烙印,还需继续努力。