一.游戏规则与要求
- 游戏内容要求:
游戏有 n 个 round,每个 round 都包括10 次 trial;
每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
每个 trial 的飞碟有随机性,总体难度随 round 上升;
鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。 - 游戏的要求:
使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
近可能使用前面 MVC 结构实现人机交互与游戏模型分离
二.游戏UML类图
三.代码说明
首先是动作控制器:前面大题的架构与上次作业中的架构基本相同,在场景控制器中使用FlyActionManager 类的函数,然后在FlyActionManager 类中调页UFOFlyAction类进行每一帧对飞碟位置的更新即可实现飞碟飞的动作。
在UFOFlyAction类中,给飞碟一个方向和一个力,然后飞碟每一帧计算下一帧做有向下加速度的飞行动作的位置,然后进行赋值即可实现的飞行动作。
最后当飞碟被点中或者飞出场景外就需要等待场景控制器和飞碟工厂进行配合回收飞碟。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameobject;
public Transform transform;
public ISSActionCallback callback;
protected SSAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
public enum SSActionEventType : int { Started, Competeted }
public interface ISSActionCallback
{
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null);
}
public class SSActionManager : MonoBehaviour, ISSActionCallback
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();
protected void Update()
{
foreach (SSAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable)
{
ac.Update();
}
}
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
waitingDelete.Clear();
}
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
{
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
}
}
public class FlyActionManager : SSActionManager{
public UFOFlyAction fly;
public Controllor scene;
protected void Start(){
scene = (Controllor)SSDirector.GetInstance ().CurrentSceneControllor;
scene.fam = this;
}
public void UFOfly(GameObject disk, float angle, float power){
fly = UFOFlyAction.GetSSAction (disk.GetComponent<DiskData> ().direction, angle, power);
this.RunAction (disk, fly, this);
}
}
public class UFOFlyAction : SSAction
{
public float gravity = -5;
private Vector3 start_vector;
private Vector3 gravity_vector = Vector3.zero;
private float time;
private Vector3 current_angle = Vector3.zero;
private UFOFlyAction() { }
public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
{
UFOFlyAction action = CreateInstance<UFOFlyAction>();
if (direction.x == -1)
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
}
else
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
}
return action;
}
public override void Update()
{
time += Time.fixedDeltaTime;
gravity_vector.y = gravity * time;
transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
transform.eulerAngles = current_angle;
if (this.transform.position.y < -10)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
接下来是飞碟工厂类:它实现了要求
使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
飞碟工厂类的目的是管理飞碟实例,同时对外屏蔽飞碟实例的的提取和回收细节。它可以根据轮数不同生产出不同的飞碟,然后对于被点中或者飞出场景外的飞碟就可以进行回收。飞碟工厂从仓库中获取这种飞碟,如果仓库中没有,则新的实例化一个飞碟,然后添加到正在使用的飞碟列表中。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour
{
public GameObject disk_prefab = null;
private List<DiskData> used = new List<DiskData>();
private List<DiskData> free = new List<DiskData>();
public GameObject GetDisk(int round)
{
float start_y = -10f;
string tag;
disk_prefab = null;
if (round == 1)
{
tag = "disk1";;
}
else if(round == 2)
{
tag = "disk2";
}
else
{
tag = "disk3";
}
for(int i=0;i<free.Count;i++)
{
if(free[i].tag == tag)
{
disk_prefab = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
if(disk_prefab == null)
{
if (tag == "disk1")
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else if (tag == "disk2")
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
disk_prefab.GetComponent<DiskData> ().score = 2;
}
else
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);
disk_prefab.GetComponent<DiskData> ().score = 3;
}
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk_prefab.GetComponent<MeshRenderer> ().material.color = disk_prefab.GetComponent<DiskData>().color;
disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0);
disk_prefab.GetComponent<DiskData> ().tag = tag;
disk_prefab.transform.localScale = disk_prefab.GetComponent<DiskData>().scale;
}
used.Add(disk_prefab.GetComponent<DiskData>());
return disk_prefab;
}
public void FreeDisk(GameObject disk)
{
for(int i = 0;i < used.Count; i++)
{
if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID())
{
used[i].gameObject.SetActive(false);
free.Add(used[i]);
used.Remove(used[i]);
break;
}
}
}
}
飞碟的参数类: 包含每个飞碟的颜色,分数,位置,大小,以及是属于哪个round的飞碟。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour
{
public int score = 1;
public Color color = Color.white;
public Vector3 direction;
public Vector3 scale = new Vector3( 1 ,0.1f, 1);
public string tag;
}
然后是场景控制器类: 它实现了接口中的函数Hit (Vector3 pos);Restart ();GetScore();GameOver ();isCounting()以及getEmitTime ()。游戏开始时通过isCounting()以及getEmitTime ()判断倒计时3秒是否完成,若倒计时结束则将counting(是否倒计时)设为false并开始发送飞碟。在每一帧中可以进行
InvokeRepeating(“LoadResources”, 1f, speed),即延时以speed的速度senddisk扔出飞碟,每次得分足够后就加速speed以更快的方式扔出飞碟,提高游戏难度。Hit函数实现了玩家通过点击发出子弹摧毁飞碟。玩家若击中飞碟则触发飞碟的粒子爆炸效果。其他函数都是与GUI交互的函数。
在此处满足了要求:
该工厂必须是场景单实例的
实现了singleton类并使用singleton类初始化工厂类,使工厂是场景单实例的。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Controllor : MonoBehaviour,ISceneControllor,IUserAction
{
public FlyActionManager fam;
public DiskFactory df;
public UserGUI ug;
public ScoreRecorder sr;
public RoundControllor rc;
private Queue<GameObject> dq = new Queue<GameObject> ();
private List<GameObject> dfree = new List<GameObject> ();
private GameObject explosion;
private float emit_time = 3;
private int round = 1;
private float t = 1;
private float speed = 2;
private int score_round = 5;
private bool flag = false;
private bool game_over = false;
private bool counting = true;
public bool isCounting(){return counting;}
public int getEmitTime(){return (int)emit_time+1;}
void Start(){
SSDirector director = SSDirector.GetInstance();
director.CurrentSceneControllor = this;
df = Singleton<DiskFactory>.Instance;
sr = gameObject.AddComponent<ScoreRecorder> () as ScoreRecorder;
fam = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
ug = gameObject.AddComponent<UserGUI>() as UserGUI;
rc = gameObject.AddComponent<RoundControllor> () as RoundControllor;
explosion = Instantiate (Resources.Load<GameObject> ("Prefabs/ParticleSystem1"), new Vector3(0, -100, 0), Quaternion.identity);
t = speed;
}
void Update ()
{
if (emit_time > 0) {
counting = true;
emit_time -= Time.deltaTime;
} else {
counting = false;
t-=Time.deltaTime;
if (t < 0) {
LoadResources ();
SendDisk ();
t = speed;
}
if ((sr.score >= 10 && round == 1) || (sr.score >= 30 && round == 2)) {
round++;
rc.loadRoundData (round);
}
}
}
public void setting(float speed_,GameObject explosion_)
{
speed = speed_;
explosion = explosion_;
}
public void LoadResources()
{
dq.Enqueue(df.GetDisk(round));
}
private void SendDisk()
{
float position_x = 16;
if (dq.Count != 0)
{
GameObject disk = dq.Dequeue();
dfree.Add(disk);
disk.SetActive(true);
float ran_y = Random.Range(1f, 4f);
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk.GetComponent<DiskData>().direction = new Vector3(ran_x, ran_y, 0);
Vector3 position = new Vector3(-disk.GetComponent<DiskData>().direction.x * position_x, ran_y, 0);
disk.transform.position = position;
float power = Random.Range(10f, 15f);
float angle = Random.Range(15f, 28f);
fam.UFOfly(disk,angle,power);
}
for (int i = 0; i < dfree.Count; i++)
{
GameObject temp = dfree[i];
if (temp.transform.position.y < -10 && temp.gameObject.activeSelf == true)
{
df.FreeDisk(dfree[i]);
dfree.Remove(dfree[i]);
ug.ReduceBlood();
}
}
}
public void Hit (Vector3 pos){
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
bool not_hit = false;
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<DiskData>() != null)
{
for (int j = 0; j < dfree.Count; j++)
{
if (hit.collider.gameObject.GetInstanceID() == dfree[j].gameObject.GetInstanceID())
{
not_hit = true;
}
}
if(!not_hit)
{
return;
}
dfree.Remove(hit.collider.gameObject);
sr.Record(hit.collider.gameObject);
explosion.transform.position = hit.collider.gameObject.transform.position;
explosion.GetComponent<ParticleSystem>().Play();
hit.collider.gameObject.transform.position = new Vector3(0, -100, 0);
df.FreeDisk(hit.collider.gameObject);
break;
}
}
}
public void Restart (){
SceneManager.LoadScene(0);
}
public int GetScore (){
return sr.score;
}
public void GameOver (){
game_over = true;
}
}
我们还需要一个记分员类记录每次的得分:它可以根据击中不同加上不同的分数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour {
public int score = 0;
void Start(){score = 0;}
public void Record(GameObject disk){
score = score + disk.GetComponent<DiskData> ().score;
}
}
**接口类:**声明了需要使用的函数提供给GUI使用并交给场景控制器实现。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneControllor{
void LoadResources ();
}
public interface IUserAction{
void Hit (Vector3 pos);
void Restart ();
int GetScore();
void GameOver ();
bool isCounting();
int getEmitTime ();
}
轮数控制器类: 实现了要求
每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
我可以在类中控制飞碟的发射间隔以及爆炸效果。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RoundControllor : MonoBehaviour {
private IUserAction action;
private float speed;
private GameObject explosion;
void Start(){
action = SSDirector.GetInstance().CurrentSceneControllor as IUserAction;
speed = 2;
}
public void loadRoundData(int round)
{
switch (round)
{
case 1:
break;
case 2:
speed = 1.5f;
explosion = Instantiate (Resources.Load<GameObject> ("Prefabs/ParticleSystem2"), new Vector3(0, -100, 0), Quaternion.identity);
action.setting (speed,explosion);
break;
case 3:
speed = 1;
explosion = Instantiate (Resources.Load<GameObject> ("Prefabs/ParticleSystem3"), new Vector3(0, -100, 0), Quaternion.identity);
action.setting (speed,explosion);
break;
}
}
}
用户界面类: GUI中实现了重新开始功能,显示生命值,得分以及游戏倒计时的功能。游戏倒计时即在场景控制器的update记录消耗的时间然后取整,若大于0则显示到屏幕上。通过接口函数就可以实现显示生命值,得分等功能。然后可以调用场景控制器中的hit实现在屏幕上发射子弹摧毁飞碟。最后直接使用SceneManager.LoadScene(0)即实现重新开始。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
private IUserAction action;
public int life = 6;
GUIStyle bold_style = new GUIStyle();
GUIStyle text_style = new GUIStyle();
GUIStyle text_style2 = new GUIStyle();
void Start ()
{
action = SSDirector.GetInstance().CurrentSceneControllor as IUserAction;
}
void OnGUI ()
{
text_style.normal.textColor = new Color(1,1,1, 1);
text_style.fontSize = 16;
text_style2.normal.textColor = new Color(1,1,1, 1);
text_style2.fontSize = 100;
if (action.isCounting ()) {
GUI.Label(new Rect(Screen.width / 2 - 40, Screen.width / 2 - 300, 50, 50), action.getEmitTime().ToString(), text_style2);
} else {
if (Input.GetButtonDown("Fire1"))
{
Vector3 pos = Input.mousePosition;
action.Hit(pos);
}
GUI.Label(new Rect(10, 5, 200, 50), "score:", text_style);
GUI.Label(new Rect(55, 5, 200, 50), action.GetScore().ToString(), text_style);
GUI.Label(new Rect(10, 30, 50, 50), "hp:", text_style);
for (int i = 0; i < life; i++)
{
GUI.Label(new Rect(40 + 10 * i, 30, 50, 50), "X", text_style);
}
if (life == 0)
{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 300, 100, 100), "GameOver!", text_style);
if (GUI.Button(new Rect(Screen.width / 2 - 60, Screen.width / 2 - 250, 100, 50), "Restart"))
{
action.Restart();
return;
}
action.GameOver();
}
}
}
public void ReduceBlood()
{
if(life > 0)
life--;
}
}
导演类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object
{
private static SSDirector _instance;
public ISceneControllor CurrentSceneControllor{ get;set;}
public static SSDirector GetInstance(){
if (_instance == null) {
_instance = new SSDirector ();
}
return _instance;
}
}
其他设定:
预制设定如下:
有三种飞碟的预设以及每种飞碟对应粒子效果的预设。
飞碟的设置:需要在每个飞碟上手动挂载飞碟的参数脚本才能使用飞碟的参数。
粒子效果:持续实现1秒且不进行循环即可实现爆炸效果。
脚本设置: 常见一个空游戏对象,在游戏对象上挂载场景控制类以及飞碟工厂类就可以运行游戏了。
这样游戏就完成了!
四.总结
在这次游戏设计中我体会到了MVC、动作管理器的作用,有了这些基本的架构,我们游戏的实现更加容易,我们只需实现场景控制器以及各个模块的控制器再通过MVC架构将他们整合就可以得到一个游戏。
游戏演示:UFO小游戏 github地址:UFO
最后再次感谢师兄的博客供我参考!