编写一个简单的鼠标打飞碟(Hit UFO)游戏
游戏内容要求:
游戏有 n 个 round,每个 round 都包括10 次 trial。
每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制。
每个 trial 的飞碟有随机性,总体难度随 round 上升。
鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求:
使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类。
尽可能使用前面 MVC 结构实现人机交互与游戏模型分离。
实现效果截图
演示视频地址
https://www.bilibili.com/video/BV1F24y117AJ/
游戏设计实现
1.首先是飞碟工厂的实现
该工厂使用Singleton单例模式实现,且需要使用带缓存的工厂,故使用 used和free 两个存飞碟的链表表示正在使用中的飞碟和空闲的飞碟。当需要工厂输出一个飞碟时,若free链表不为空,则从free链表中寻找相应类型的飞碟,若没有空闲的飞碟,则创建一个对应类型type的飞碟并且放入used链表中,并设置其active为true;
在游戏开始时会不断创建飞碟,而是储存起来,简单将其active设置为false,可以使它隐形,再次使用则初始化位置即可,从而能够循环使用;
对飞碟的回收 FreeDisk也就是将使用完的(运动至屏幕下方看不见的)原本在used中转到free中。
代码如下。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour {
private List<Disk> used = new List<Disk>();
private List<Disk> free = new List<Disk>();
public GameObject GetDisk(int type) {
GameObject disk_prefab = null;
//寻找空闲飞碟,如有空闲则不需要多实例化一个
if (free.Count>0) {
for(int i = 0; i < free.Count; i++) {
if (free[i].type == type) {
disk_prefab = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
}
if(disk_prefab == null) {
if(type == 1) {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk1"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
else if (type == 2) {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk2"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
else {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk3"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<Disk>().color;
}
used.Add(disk_prefab.GetComponent<Disk>());
disk_prefab.SetActive(true);
return disk_prefab;
}
public void FreeDisk() {
for(int i=0; i<used.Count; i++) {
if (used[i].gameObject.transform.position.y <= -10f) {
free.Add(used[i]);
used.Remove(used[i]);
}
}
}
public void Reset() {
FreeDisk();
}
}
2.动作管理类实现
SSActionManager 类 -动作管理基类
actions -执行的动作字典,以GetInstanceID的返回值为key ,SSAction为值
waitingAdd 等待执行动作 waitingDelete 待删除列表
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/*动作管理基类*/
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();
//对于字典中每一个pair,看是执行还是删除
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);
Object.Destroy(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) {
}
}
动作基类SSAction
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; //动作对象的transform
public ISSActionCallback callback; //回调函数
//防止用户自己new对象
protected SSAction() { }
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
3.设计控制游戏的运行的导演类
FirstController类
这里引入了 飞行动作管理的action_manage,和飞碟工厂disk_factory,以及用户界面UserGUI 和计分类 score_recorder, 并在Start()中将其均初始化,disk_factory与score_recorder均用单例模式。
Update()主要实现飞碟在界面中飞行,以及不同种飞碟出现的时间和位置,通过SendDisk(type) 来从飞碟工厂中拿出对应的飞碟,且检测鼠标的点击是否击中飞碟Hit(pos),若击中则累计相应的分数:Disk1紫色飞碟加一分,Disk2绿色飞碟加两分,Disk3红色飞碟加三分
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
public FlyActionManager action_manager;
public DiskFactory disk_factory;
public UserGUI user_gui;
public ScoreRecorder score_recorder;
private int round = 1;
private int trial = 0;
private float speed = 1f;
private bool running = false;
void Start () {
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
disk_factory = Singleton<DiskFactory>.Instance;
score_recorder = Singleton<ScoreRecorder>.Instance;
action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
}
int count = 0;
void Update () {
if(running) {
count++;
if (Input.GetButtonDown("Fire1")) {
Vector3 pos = Input.mousePosition;
Hit(pos);
}
switch (round) {
case 1: {
if (count >= 150) {
count = 0;
SendDisk(1);
trial += 1;
if (trial == 10) {
round += 1;
trial = 0;
}
}
break;
}
case 2: {
if (count >= 100) {
count = 0;
if (trial % 2 == 0) SendDisk(1);
else SendDisk(2);
trial += 1;
if (trial == 10) {
round += 1;
trial = 0;
}
}
break;
}
case 3: {
if (count >= 50) {
count = 0;
if (trial % 3 == 0) SendDisk(1);
else if(trial % 3 == 1) SendDisk(2);
else SendDisk(3);
trial += 1;
if (trial == 10) {
running = false;
}
}
break;
}
default:break;
}
disk_factory.FreeDisk();
}
}
public void LoadResources() {
disk_factory.GetDisk(round);
disk_factory.FreeDisk();
}
private void SendDisk(int type) {
//从工厂中拿一个飞碟
GameObject disk = disk_factory.GetDisk(type);
//飞碟位置
float ran_y = 0;
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
//飞碟初始所受的力和角度
float power = 0;
float angle = 0;
if (type == 1) {
ran_y = Random.Range(1f, 5f);
power = Random.Range(5f, 7f);
angle = Random.Range(25f,30f);
}
else if (type == 2) {
ran_y = Random.Range(2f, 3f);
power = Random.Range(10f, 12f);
angle = Random.Range(15f, 17f);
}
else {
ran_y = Random.Range(5f, 6f);
power = Random.Range(15f, 20f);
angle = Random.Range(10f, 12f);
}
disk.transform.position = new Vector3(ran_x*16f, ran_y, 0);
action_manager.DiskFly(disk, angle, power);
}
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) {
score_recorder.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
}
}
}
public float GetScore() {
return score_recorder.GetScore();
}
public int GetRound() {
return round;
}
public int GetTrial() {
return trial;
}
//重新开始
public void ReStart() {
running = true;
score_recorder.Reset();
disk_factory.Reset();
round = 1;
trial = 1;
speed = 2f;
}
//游戏结束
public void GameOver() {
running = false;
}
}
4.其他游戏相关类实现(部分)
计分类 ScoreRecorder
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/*记录分数*/
public class ScoreRecorder : MonoBehaviour {
private float score;
void Start () {
score = 0;
}
public void Record(GameObject disk) {
score += disk.GetComponent<Disk>().score;
}
public float GetScore() {
return score;
}
public void Reset() {
score = 0;
}
}
用户开始界面UserGUI类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour {
private IUserAction action;
//每个GUI的style
GUIStyle bold_style = new GUIStyle();
GUIStyle text_style = new GUIStyle();
GUIStyle over_style = new GUIStyle();
private bool game_start = false;
void Start () {
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
void OnGUI () {
bold_style.normal.textColor = new Color(1, 0, 0);
bold_style.fontSize = 16;
text_style.normal.textColor = new Color(0, 0, 0, 1);
text_style.fontSize = 16;
over_style.normal.textColor = new Color(1, 0, 0);
over_style.fontSize = 25;
if (game_start) {
GUI.Label(new Rect(Screen.width - 150, 5, 200, 50), "分数:"+ action.GetScore().ToString(), text_style);
GUI.Label(new Rect(100, 5, 50, 50), "Round:" + action.GetRound().ToString(), text_style);
GUI.Label(new Rect(180, 5, 50, 50), "Trial:" + action.GetTrial().ToString(), text_style);
if (action.GetRound() == 3 && action.GetTrial() == 10) {
GUI.Label(new Rect(Screen.width / 2 - 20, Screen.height / 2 - 250, 100, 100), "游戏结束", over_style);
GUI.Label(new Rect(Screen.width / 2 - 10, Screen.height / 2 - 200, 50, 50), "你的分数:" + action.GetScore().ToString(), text_style);
if (GUI.Button(new Rect(Screen.width / 2 - 20, Screen.height / 2 - 150, 100, 50), "重新开始")) {
action.ReStart();
return;
}
action.GameOver();
}
}
else {
GUI.Label(new Rect(Screen.width / 2 - 50, 100, 100, 100), "简单打飞碟", over_style);
GUI.Label(new Rect(Screen.width / 2 - 50, 150, 100, 100), "鼠标点击飞碟", text_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, 200, 100, 50), "游戏开始")) {
game_start = true;
action.ReStart();
}
}
}
}
5.预制体资源设置
Disk1,Disk2,Disk3的区分在于Transform的position 和 Type 与Score(即载入脚本SendDisk方法等用到的对应的类型和计分)以及Color的设计,具体如图(以Disk1为例)
调整position,Type与Score均为1 ,设为紫色。其余两种同理
Unity 3d打飞碟的设计如上。