编写一个简单的树表打飞碟(Hit UFO)游戏
游戏内容要求
1.游戏有 n 个 round,每个 round 都包括10 次 trial。
2.每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制。
3.每个 trial 的飞碟有随机性,总体难度随 round 上升。
4.鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。游戏设计要求
1.使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类。
2.尽可能使用前面 MVC 结构实现人机交互与游戏模型分离。
具体代码及演示视频见项目地址
根据项目可以设计整体框架如下:
1.导演、场记相关:SSDirector单例模式、ISceneController场景接口、FirstController游戏控制器。
2.用户相关:IUserAction用户动作接口、UserGUI用户界面。
3.飞碟生成相关:DiskData数据类型、DiskFactory工厂模式。
4.动作管理相关:SSActionManager动作管理基类、IActionManager飞行动作管理的接口、CCActionManager运动学动作管理。
4.飞行动作相关:SSAction动作基类、ISSActionCallBack动作结束的回调接口、CCFlyAction动作实现。
5.辅助类:Singleton单例模板、ScoreRecorder记分员。
天空盒
预制
脚本
(一)Actions动作与动作管理器
1.SSAction动作基类
public class SSAction : ScriptableObject
{
public bool enable = true; //是否可进行
public bool destroy = false; //是否已完成
public GameObject gameobject; //动作对象
public Transform transform; //动作对象的transform
public ISSActionCallback callback; //回调函数
protected SSAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
2.SSActionManager 动作管理类基类
public class SSActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //动作字典
private List<int> waitingDelete = new List<int>(); //等待删除的key列表
private List<SSAction> waitingAdd = new List<SSAction>(); //等待执行的动作列表
protected void Update()
{
//将等待执行的动作加入字典并清空待执行列表
foreach (SSAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();
//遍历字典中的动作,分执行/删除
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ssac = kv.Value;
if (ssac.enable)
{
ssac.Update();
}
else if (ssac.destroy)
{
waitingDelete.Add(ssac.GetInstanceID());
}
}
//删除所有已完成的动作并清空待删除列表
foreach (int key in waitingDelete)
{
SSAction ssac = actions[key];
actions.Remove(key);
Object.Destroy(ssac);
}
waitingDelete.Clear();
}
//外界只需要调用动作管理类的RunAction函数即可完成动作。
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
{
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
}
3.CCFlyAction 飞碟动作类
public class CCFlyAction : SSAction
{
float speed;
Vector3 direction;
public static CCFlyAction GetSSAction(Vector3 dir, float speed)
{
CCFlyAction tmp = ScriptableObject.CreateInstance<CCFlyAction>();
tmp.speed = speed;
tmp.direction = dir;
return tmp;
}
public override void Start()
{
gameobject.GetComponent<Rigidbody>().isKinematic = false;
//为物体增加水平初速度,否则会由于受到重力严重影响运动状态。
gameobject.GetComponent<Rigidbody>().velocity = speed * direction;
}
public override void Update()
{
//动作运行
transform.Translate(direction * speed * Time.deltaTime);
//判断飞碟是否落地
if (this.transform.position.y < -10)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
- CCActionManager 动作管理类
动作结束时会调用动作管理者实现的回调函数,即IActionCallback接口中的SSActionEventType,动作管理者将动作绑定的游戏对象(飞碟)销毁。
public class CCActionManager : SSActionManager, ISSActionCallback, IActionManager
{
public CCFlyAction Action;
public FirstController controller;
public void Start()
{
controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
}
public void Fly(GameObject disk, float speed, Vector3 direction)
{
Action = CCFlyAction.GetSSAction(direction, speed);
RunAction(disk, Action, this);
}
//回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
controller.diskFactory.FreeDisk(source.gameobject);//释放资源
}
}
5.IActionCallback 事件回调接口
public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
//回调函数
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
6.IActionManager动作管理接口
接口只定义一个函数,便于采用Adapter模式适应不同的接口
public interface IActionManager
{
void Fly(GameObject disk, float speed, Vector3 direction);
}
(二)Disk相关
1.DiskFactory 飞碟工厂
GetDisk被主控制器调用,round会影响所生产的飞碟的速度、大小等属性,round越高游戏难度越大。
有两个列表use和free,存放的是飞碟属性(包括分数、速度)。
飞碟初始位置、颜色、速度大小以及方向都随机。
public class DiskFactory : MonoBehaviour
{
public GameObject disk_prefab;
private List<DiskData> use; //已使用的飞碟
private List<DiskData> free; //空闲的飞碟
void Start()
{
use = new List<DiskData>();
free = new List<DiskData>();
disk_prefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/UFO"), Vector3.zero, Quaternion.identity);
disk_prefab.SetActive(false);
}
//获取飞碟
public GameObject GetDisk(int round)
{
GameObject disk;
//如果有空闲的飞碟,则直接使用。否则生成新的飞碟
if (free.Count > 0)
{
disk = free[0].gameObject;
free.Remove(free[0]);
}
else
{
disk = GameObject.Instantiate<GameObject>(disk_prefab, Vector3.zero, Quaternion.identity);
disk.AddComponent<DiskData>();
}
disk.gameObject.name = "Disk";
//生成飞碟的规则
float base_speed = 3 + round * 0.8f; //round越大,速度基数越大,难度越高
Vector3 base_scale = new Vector3(8, 1, 8); //颜色和大小有关
float rand_y = Random.Range(-0.5f, 0.5f); //垂直方向速度随机生成
int side = Random.Range(0, 2); //水平速度方向
if (side == 0)
{
side = -1;
}
int rand_num = Random.Range(1, 4); //颜色和速度有关
int[] point_str = { 1, 2, 3 };
float[] speed_str = { 0.6f, 1.2f, 1.8f };
float[] localScale_str = { 1.2f, 1f, 0.8f };
disk.GetComponent<DiskData>().points = point_str[rand_num - 1];
disk.GetComponent<DiskData>().speed = speed_str[rand_num - 1] * base_speed;
disk.GetComponent<Transform>().localScale = localScale_str[rand_num - 1] * base_scale;
disk.GetComponent<DiskData>().direction = new Vector3(side, rand_y, 0);
if (rand_num == 1)
disk.GetComponent<Renderer>().material.color = Color.red;
else if (rand_num == 2)
disk.GetComponent<Renderer>().material.color = Color.black;
else
disk.GetComponent<Renderer>().material.color = Color.white;
use.Add(disk.GetComponent<DiskData>());
return disk;
}
//释放
public void FreeDisk(GameObject disk)
{
foreach (DiskData d in use)
{
if (d.gameObject.GetInstanceID() == disk.GetInstanceID())
{
disk.SetActive(false); //先灭活
use.Remove(d);
free.Add(d); //重新放回到对象池中
break;
}
}
}
//销毁所有对象
public void Clear()
{
foreach (DiskData disk in free)
{
disk.gameObject.transform.position = new Vector3(0, -20, 0);
Destroy(disk.gameObject);
}
free.Clear();
foreach (DiskData disk in use)
{
//让FlyAction结束并回调,再销毁对象。
disk.gameObject.transform.position = new Vector3(0, -20, 0);
Destroy(disk.gameObject);
}
use.Clear();
}
}
2.DiskData 飞碟数据
public class DiskData : MonoBehaviour
{
public float speed; //水平速度
public int points; //击中红碟1分,黑碟2分,白碟3分
public Vector3 direction; //初始方向
}
(三)辅助类
1.Singleton单例模板
与教材相同
public class Singleton<T> : MonoBehaviour where T: MonoBehaviour
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T) + " is needed in the scene, but there is none");
}
}
return instance;
}
}
}
2.ScoreRecorder计分者
public class ScoreRecorder : System.Object{
public int score;
public void Record(GameObject disk){
score += disk.GetComponent<DiskData>().points;
}
public void Reset(){
score = 0;
}
}
(四)FirstController主控制器
FirstController负责初始化对象,从飞碟工厂中获取飞碟、回合控制、处理玩家击中、飞碟出界事件。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public DiskFactory diskFactory;
public IActionManager actionManager;
public ScoreRecorder scoreRecorder;
public UserGUI userGUI;
int round; //回合
float curr_time; //当前时间,用于update的累计计算
int curr_disk_num; //该回合飞碟数量
void Start()
{
//添加导演
SSDirector.GetInstance().CurrentScenceController = this;
//添加脚本,初始化对象
gameObject.AddComponent<DiskFactory>();
gameObject.AddComponent<CCActionManager>();
userGUI = gameObject.AddComponent<UserGUI>();
diskFactory = Singleton<DiskFactory>.Instance;
actionManager = Singleton<CCActionManager>.Instance;
//加载其他资源
LoadResources();
}
public void LoadResources()
{
scoreRecorder = new ScoreRecorder();
scoreRecorder.Reset();
round = 0;
curr_time = 0;
curr_disk_num = 0;
}
//发射飞碟
public void SendDisk()
{
GameObject disk = diskFactory.GetDisk(round);
float side = -disk.GetComponent<DiskData>().direction.x; //与速度水平方向相反
disk.transform.position = new Vector3(side * 15f, UnityEngine.Random.Range(0f, 8f), 0);
disk.SetActive(true); //激活
actionManager.Fly(disk, disk.GetComponent<DiskData>().speed, disk.GetComponent<DiskData>().direction);//设置飞行动作
}
//击中飞碟
public void Click(Vector3 position)
{
Camera camera = Camera.main;
Ray ray = camera.ScreenPointToRay(position);
RaycastHit[] Hit = Physics.RaycastAll(ray);
for (int i = 0; i < Hit.Length; i++)
{
if (Hit[i].collider.gameObject.GetComponent<DiskData>() != null)
{
GameObject disk = Hit[i].collider.gameObject;
disk.transform.position = new Vector3(0, -20, 0);
diskFactory.FreeDisk(disk);
scoreRecorder.Record(disk);
userGUI.score = scoreRecorder.score;
}
}
}
//清除界面中的飞碟
public void ClearDisk()
{
GameObject[] obj = FindObjectsOfType(typeof(GameObject)) as GameObject[]; //获取所有gameobject元素
foreach (GameObject child in obj)
{
if (child.gameObject.name == "Disk")
{
child.gameObject.SetActive(false);
}
}
diskFactory.Clear(); //彻底销毁
}
public void Reset()
{
round = 0;
curr_time = 0;
curr_disk_num = 0;
userGUI.round = round;
scoreRecorder.Reset();
userGUI.score = scoreRecorder.score;
ClearDisk();
}
void Update()
{
if (userGUI.status == 1)
{
if (round <= 10) //游戏未结束
{
curr_time += Time.deltaTime;
if (curr_time > 2) //每2秒生成一次
{
curr_time = 0;
//游戏初始化
if (round == 0)
{
round++;
userGUI.round = round;
}
int rand_disk = Random.Range(1, 5); //随机发射1-4个飞碟
for (int i = 0; i < rand_disk; i++)
{
SendDisk();
curr_disk_num++;
if (curr_disk_num == 10)
{
break;
}
}
if (curr_disk_num == 10 && round <= 10) //发射10个后更新轮次
{
curr_disk_num = 0;
round++;
userGUI.round = round;
}
}
}
if (round == 11) //结束游戏
{
userGUI.round = 10;
curr_time += Time.deltaTime;
if (curr_time > 5)
{
userGUI.status = 2;
}
}
}
}
}
其余代码大多沿用魔鬼与牧师,具体见项目地址