打飞碟
编写一个简单的鼠标打飞碟(Hit UFO)游戏
要求
- 游戏有 n 个 round,每个 round 都包括10 次 trial
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离
代码结构
还是采用MVC结构,具体可参考牧师与魔鬼的制作,本次主要是利用工厂模式完成不同飞碟的生产和回收。
代码说明
本次的核心代码主要是Disk和DiskFactory两部分,利用工厂模式来控制物体的产生和复用
对于Disk的实现,我们主要是定义其的属性以及相对应的get和set方法:
public class Disk : MonoBehaviour
{
public Vector3 StartPoint
{
get
{
return gameObject.transform.position;
}
set
{
gameObject.transform.position = value;
}
}
public Vector3 Direction
{
get
{
return Direction;
}
set
{
gameObject.transform.Rotate(value);
}
}
public Color color
{
get
{
return gameObject.GetComponent<Renderer>().material.color;
}
set
{
gameObject.GetComponent<Renderer>().material.color = value;
}
}
public float speed { get; set; }
}
对于DiskFactory,也就是Disk的制造工厂,屏蔽了生产和销毁的业务逻辑,只讲使用的结构提供给用户,使程序易于扩展。在代码中,我们还让DiskFactory负责在生产Disk时随机指定起始位置,方向,速度,颜色
public class DiskFactory
{
public GameObject diskPrefab;
public static DiskFactory DF = new DiskFactory();
private Dictionary<int, Disk> used = new Dictionary<int, Disk>();
private List<Disk> free = new List<Disk>();
private DiskFactory()
{
diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk"));
diskPrefab.AddComponent<Disk>();
diskPrefab.SetActive(false);
}
public void FreeDisk()
{
foreach (Disk x in used.Values)
{
if (x.gameObject.activeSelf == false)
{
free.Add(x);
used.Remove(x.GetInstanceID());
return;
}
}
}
public Disk GetDisk(int round)
{
FreeDisk();
GameObject newObject = null;
Disk newDisk;
if (free.Count > 0)
{
newObject = free[0].gameObject;
free.Remove(free[0]);
}
else
newObject = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
newObject.SetActive(true);
newDisk = newObject.AddComponent<Disk>();
int swith;
float s;
if (round == 1)
{
swith = Random.Range(0, 3);
s = Random.Range(30, 40);
}
else if (round == 2)
{
swith = Random.Range(0, 4);
s = Random.Range(40, 50);
}
else
{
swith = Random.Range(0, 6);
s = Random.Range(50, 60);
}
float PointX = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
newDisk.Direction = new Vector3(PointX, 1, 0);
switch (swith)
{
case 0:
newDisk.color = Color.yellow;
newDisk.speed = s;
newDisk.StartPoint = new Vector3(Random.Range(-130, -110), Random.Range(30, 90), Random.Range(110, 140));
break;
case 1:
// ...
case 2:
// ...
case 3:
// ...
case 4:
// ...
case 5:
// ...
}
used.Add(newDisk.GetInstanceID(), newDisk);
newDisk.name = newDisk.GetInstanceID().ToString();
return newDisk;
}
}
在运动分离部分,主要的修改就是重写CCMoveToAction,不同于牧师和魔鬼的上船下船,这里的运动更加简单,只需要让Disk做类似有水平初速自由落体的运动即可。
public class CCMoveToAction : SSAction
{
public float speedx;
public float speedy = 0;
private CCMoveToAction() {}
public static CCMoveToAction getAction(float speedx)
{
CCMoveToAction action = CreateInstance<CCMoveToAction>();
action.speedx = speedx;
return action;
}
public override void Start() {}
public override void Update()
{
this.transform.position += new Vector3(speedx * Time.deltaTime, -speedy * Time.deltaTime + (float)-0.5 * 10 * Time.deltaTime * Time.deltaTime, 0);
speedy += 10 * Time.deltaTime;
if (transform.position.z == -1)
{
Debug.Log("Hit");
destroy = true;
CallBack.SSActionCallback(this, true);
}
else if (transform.position.y <= -45)
{
Debug.Log("Missing");
destroy = true;
CallBack.SSActionCallback(this, false);
}
}
}
运动管理类CCActionManager:
public class CCActionManager : SSActionManager, SSActionCallback
{
int count = 0;
public SSActionEventType Complete = SSActionEventType.Completed;
UserAction UserActionController;
public void MoveDisk(Disk Disk)
{
count ++;
Complete = SSActionEventType.Started;
CCMoveToAction action = CCMoveToAction.getAction(Disk.speed);
addAction(Disk.gameObject, action, this);
}
public void SSActionCallback(SSAction source, bool isHit)
{
count --;
Complete = SSActionEventType.Completed;
UserActionController = SSDirector.getInstance().currentScenceController as UserAction;
if (!isHit)
{
UserActionController.ReduceHealth();
}
source.gameObject.SetActive(false);
}
public bool IsAllFinished()
{
Debug.Log("isALLFInished");
if (count == 0)
return true;
else return false;
}
}
在SSAction类中,还是和牧师与魔鬼差不多的写法:
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameObject;
public Transform transform;
public SSActionCallback CallBack;
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
在SSActionManager类中,我们需要注意的是,一次性可能会有很多个飞碟在一起移动,所以我们实现了三个数据结构,一个等待的队列让每个新飞碟动作等待被调用,一个字典让每个飞碟运动的id和对应的action进行对应,每次进行遍历来调用其中的update函数实现飞碟的运动。最后是等待被删除的队列,当飞碟被击中或者移动出边界我们就让这个飞碟入队,等待执行销毁。至于判断游戏结束以及游戏结束之后的处理,我们下面再讲述。
public class SSActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingToAdd = new List<SSAction>();
private List<int> watingToDelete = new List<int>();
UserAction UserActionController;
private void Start()
{
UserActionController = SSDirector.getInstance().currentScenceController as UserAction;
}
protected void Update()
{
if (UserActionController.GetHealth() <= 0)
{
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
ac.gameObject.transform.position = new Vector3(0, -5, 0);
watingToDelete.Add(ac.GetInstanceID());
}
foreach (int key in watingToDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);
}
watingToDelete.Clear();
return;
}
foreach (SSAction ac in waitingToAdd)
{
actions[ac.GetInstanceID()] = ac;
}
waitingToAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
watingToDelete.Add(ac.GetInstanceID());
}
else if (ac.enable)
{
ac.Update();
}
}
foreach (int key in watingToDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);
}
watingToDelete.Clear();
}
public void addAction(GameObject gameObject, SSAction action, SSActionCallback ICallBack)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.CallBack = ICallBack;
waitingToAdd.Add(action);
action.Start();
}
}
接口空间如下所示:
namespace Interfaces
{
public interface ISceneController
{
void LoadResources();
}
public interface UserAction
{
void Hit(Vector3 pos);
int GetScore();
int GetRound();
int GetHealth();
void ReduceHealth();
void GameOver();
bool RoundStop();
void Restart();
}
public enum SSActionEventType : int { Started, Completed }
public interface SSActionCallback
{
void SSActionCallback(SSAction source, bool isHit);
}
}
最后我们重写场记,导演和之前是一样的,可以参考牧师与魔鬼的导演,这里就不再列出。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Interfaces;
public class FirstSceneController : MonoBehaviour, ISceneController, UserAction
{
int score = 0;
int round = 1;
int tral = 0;
int health = 5;
bool start = false;
bool gameOver = false;
CCActionManager Manager;
DiskFactory DF;
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentScenceController = this;
DF = DiskFactory.DF;
Manager = GetComponent<CCActionManager>();
}
// Use this for initialization
void Start()
{
}
// Update is called once per frame
int count = 0;
void Update()
{
if (health <= 0)
gameOver = true;
if (gameOver)
return;
if (start == true)
{
count ++;
if (count >= 80)
{
count = 0;
if (DF == null)
{
Debug.LogWarning("DF is NUll!");
return;
}
tral ++;
Disk d = DF.GetDisk(round);
Manager.MoveDisk(d);
if (tral == 10)
{
round ++;
tral = 0;
}
}
}
}
public void LoadResources()
{
}
public void Hit(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i ++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<Disk>() != null)
{
Color c = hit.collider.gameObject.GetComponent<Renderer>().material.color;
if (c == Color.yellow)
score += 1;
if (c == Color.red)
score += 2;
if (c == Color.black)
score += 3;
GameObject explosion = Instantiate(Resources.Load<GameObject>("Prefabs/ParticleSystem"), hit.collider.gameObject.transform.position, Quaternion.identity);
explosion.GetComponent<ParticleSystem>().Play();
Object.Destroy(explosion, 0.1f);
hit.collider.gameObject.transform.position = new Vector3(0, -400, -1);
}
}
}
public int GetScore()
{
return score;
}
public int GetRound()
{
return round;
}
public int GetHealth()
{
return health;
}
public void ReduceHealth()
{
health -= 1;
if (health < 0)
health = 0;
}
public void GameOver()
{
gameOver = true;
}
public bool RoundStop()
{
if (round > 3 || health <= 0)
{
start = false;
return Manager.IsAllFinished();
}
else
return false;
}
public void Restart()
{
score = 0;
round = 1;
health = 5;
start = true;
gameOver = false;
}
}
还有一个比较细节的部分是,当生命值为0了之后,此时屏幕上可能还会有正在移动的飞碟,如果我们不对它们进行处理,可能会影响到下一句的游戏,这不是我们所希望的。所以我们判断到游戏结束之后,要在SSActionManager类中将所有飞碟移出屏幕并且清空其动作。再次重新开始游戏的时候就不会影响到下一局的游戏了。
在界面交互部分。可以根据自己的喜爱进行调整:
public class InterfaceGUI : MonoBehaviour
{
UserAction UserActionController;
public GameObject t;
bool isStart = false;
bool gameOver = false;
float S;
float Now;
int round = 1;
// Use this for initialization
void Start()
{
UserActionController = SSDirector.getInstance().currentScenceController as UserAction;
S = Time.time;
}
private void OnGUI()
{
if (gameOver)
{
isStart = false;
GUI.Label(new Rect(Screen.width / 2 - 30, Screen.height / 2 - 50, 100, 20), "GameOver!");
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 30, 100, 50), "Confirm"))
{
gameOver = false;
return;
}
return;
}
if (!isStart)
S = Time.time;
GUI.Label(new Rect(10, 10, 200, 20), "Time: " + ((int)(Time.time - S)).ToString());
GUI.Label(new Rect(10, 30, 200, 20), "Round: " + round);
GUI.Label(new Rect(10, 50, 200, 20), "Score: " + UserActionController.GetScore().ToString());
GUI.Label(new Rect(10, 70, 200, 20), "Health: " + UserActionController.GetHealth().ToString());
if (!isStart && GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 30, 100, 50), "Start"))
{
S = Time.time;
isStart = true;
UserActionController.Restart();
}
if (isStart)
{
round = UserActionController.GetRound();
if (Input.GetButtonDown("Fire1"))
{
Vector3 pos = Input.mousePosition;
UserActionController.Hit(pos);
}
if (round > 3)
{
round = 3;
if (UserActionController.RoundStop())
{
isStart = false;
}
}
}
if (isStart && UserActionController.GetHealth() == 0)
{
gameOver = true;
}
}
}
实现效果
我们为了效果好看,也加入了天空盒。
我们还利用了粒子效果模拟打中飞碟之后的爆炸效果,这需要我们预制一个Particle System,在GameObject -> Effects里进行添加,然后设置粒子的数量以及容器的模样,并在代码中设置0.1s之后自动销毁:
GameObject explosion = Instantiate(Resources.Load<GameObject>("Prefabs/ParticleSystem"), hit.collider.gameObject.transform.position, Quaternion.identity);
explosion.GetComponent<ParticleSystem>().Play();
Object.Destroy(explosion, 0.1f);
最终的效果如下:
编写一个简单的自定义组件
用自定义组件定义几种飞碟,做成预制:
第一种比较简单的方法是直接将属性设置成public,然后就可以在菜单中找到对应的,比如官方文档给的一个例子:
using UnityEngine;
using System.Collections;
// This is not an editor script.
public class MyPlayer : MonoBehaviour
{
public int armor = 75;
public int damage = 25;
public GameObject gun;
void Update()
{
// Update logic here...
}
}
这样我们就能够直接在菜单中设置armor,damage,gun三种属性了:
这里也可以使用Editor的方式,比如将飞碟的颜色属性加入到组件里面去:
using UnityEngine;
using UnityEditor;
using System.Collections;
[CustomEditor(typeof(Disk))]
public class DiskEditor : Editor
{
private Disk _target { get { return target as Disk; } }
public override void OnInspectorGUI()
{
_target.color = EditorGUILayout.ColorField(new GUIContent("ColorValue"), _target.color);
}
}
一个比较全面的属性设置如下所示。
我们定义的变量类型可以如下:
public int intValue;
public float floatValue;
public string stringValue;
public bool boolValue;
public Vector3 vector3Value;
public Course enumValue = Course.Chinese;
public Color colorValue = Color.white;
public Texture textureValue;
在Editor文件中,各种类型的变量添加组件的方式:
_target.intValue = EditorGUILayout.IntField("IntValue", _target.intValue);
_target.floatValue = EditorGUILayout.FloatField("FloatValue", _target.floatValue);
_target.stringValue = EditorGUILayout.TextField("StringValue", _target.stringValue);
_target.boolValue = EditorGUILayout.Toggle("BoolValue", _target.boolValue);
_target.vector3Value = EditorGUILayout.Vector3Field("Vector3Value", _target.vector3Value);
_target.enumValue = (Course)EditorGUILayout.EnumPopup("EnumValue", (Course)_target.enumValue);
_target.colorValue = EditorGUILayout.ColorField(new GUIContent("ColorValue"), _target.colorValue);
_target.textureValue = (Texture)EditorGUILayout.ObjectField("TextureValue", _target.textureValue, typeof(Texture), true);