游戏简介
在打飞碟游戏中,玩家可以通过点击飞出的飞碟得分,目标是争取获得更高的分。
游戏规则
- 游戏有 n 个 round,每个 round 都包括10 次 trial。
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制。
- 每个 trial 的飞碟有随机性,总体难度随 round 上升。
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
项目实现要求
- 必须使用对象池管理飞碟对象。
- 建议使用 ScriptableObject 配置不同的飞碟
- 建议使用物理引擎管理飞碟飞行路径。
项目实现效果
计分规则
加分项 | 加分 |
---|---|
飞碟为蓝色,大小为3 | 加1分 |
飞碟为绿色,大小为2 | 加2分 |
飞碟为红色,大小为1 | 加3分 |
飞碟速度为x(都是偶数) | 原有基础上加x/2分 |
类图
截图及视频
游戏开始界面
运动学模式
物理模式 游戏结算页面
视频展示
HIT UFO
具体代码
SSDirector
public class SSDirector : System.Object
{
// singlton instance
private static SSDirector _instance;
public MainController currentController { get; set; }
// get instance anytime anywhare!
public static SSDirector getInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
Singleton
这是为实现场景单实例写的Singleton单实例模板类
using UnityEngine;
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;
}
}
}
DiskFactory
该类主要实现了飞碟的生成和回收,实现了对象池的任务,通过使用两个列表:已使用和未使用的飞碟,使得飞碟对象不需要反复生成和销毁。
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour
{
private List<Disk> dirtyDisk; // 正在被使用的disk
private List<Disk> freeDisk; // 没有被使用的disk
// Start is called before the first frame update
void Start()
{
dirtyDisk = new List<Disk>();
freeDisk = new List<Disk>();
}
// 获取飞碟
public GameObject GetDisk(Ruler ruler)
{
GameObject disk;
// 如果freeDisk中有能用的就直接用,没有就新创建
int diskCount = freeDisk.Count;
if (diskCount == 0)
{
disk = GameObject.Instantiate(
Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
disk.AddComponent(typeof(Disk));
}
else
{
disk = freeDisk[diskCount - 1].gameObject;
freeDisk.Remove(freeDisk[diskCount - 1]);
}
// 设置速度、颜色、大小、飞入方向
disk.GetComponent<Disk>().speed = ruler.speed;
disk.GetComponent<Disk>().color = ruler.color;
disk.GetComponent<Disk>().size = ruler.size;
// 给飞碟上颜色
if (ruler.color == "red")
{
disk.GetComponent<Renderer>().material.color = Color.red;
}
else if (ruler.color == "green")
{
disk.GetComponent<Renderer>().material.color = Color.green;
}
else
{
disk.GetComponent<Renderer>().material.color = Color.blue;
}
// 绘制飞碟大小
disk.transform.localScale = new Vector3(ruler.size, 0.2f, ruler.size);
// 选择飞碟飞入屏幕的起始位置
disk.transform.position = ruler.beginPos;
// 设置飞碟显示
disk.SetActive(true);
// 将飞碟加入使用队列
dirtyDisk.Add(disk.GetComponent<Disk>());
return disk;
}
// 飞碟回收方法,将不使用的飞碟从使用队列放到空闲队列中
public void FreeDisk(GameObject disk)
{
foreach (Disk d in dirtyDisk)
{
if (d.gameObject.GetInstanceID() == disk.GetInstanceID())
{
disk.SetActive(false);
dirtyDisk.Remove(d);
freeDisk.Add(d);
break;
}
}
}
// Update is called once per frame
void Update()
{
}
}
Disk
该类定义了飞碟所需要的参数。
using UnityEngine;
public class Disk : MonoBehaviour
{
public int size; // 大小
public string color; // 颜色
public int speed; // 发射速度
}
ScoreRecorder
该类定义了加分规则。
public class ScoreRecorder
{
public int score; // 游戏分数
public ScoreRecorder()
{
score = 0;
}
// 记录分数,根据点击中的飞碟的大小,速度计算得分
public void Record(Disk disk)
{
// 飞碟大小与颜色相对应,最小为红色,然后黄色,最后蓝色
// 颜色为红色得3分,颜色为黄色得2分,颜色为蓝色得1分
int diskSize = disk.size;
if(diskSize == 1)
{
score += 3;
}
else if(diskSize == 2)
{
score += 2;
}
else
{
score += 1;
}
// 加速度的分
score += disk.speed / 2;
}
// 重置分数,设为0
public void Reset()
{
score = 0;
}
}
RoundController
该类确定了运动类型的使用,初始化基本参数(游戏进行多少round、每trail多少个飞碟),定义了飞碟的生成、释放。
该类中的LaunchDisk,是整个项目的核心算法,实现了对每个飞碟属性的设置。
该类的Update函数同样是整个项目的核心算法,实现了对发射时间间隔的控制、游戏是否结束的判断等等。
该类中有一个FreeAllFactoryDisk函数,是用于在切换运动类型时,将前一种类型的飞碟全部清除。这样做的目的是,两种运动类型的飞碟属性是不同的,所以不能让上一种类型的飞碟影响现在的游戏。
using UnityEngine;
public class RoundController : MonoBehaviour
{
private IActionManager actionManager; // 选择飞碟的运动类型
private ScoreRecorder scoreRecorder; // 记分器
private MainController mainController;
private Ruler ruler; // 飞碟获取规则
void Start()
{
// 一开始飞碟的运动类型默认为运动学运动
actionManager = gameObject.AddComponent<CCActionManager>();
gameObject.AddComponent<PhysisActionManager>();
scoreRecorder = new ScoreRecorder();
mainController = SSDirector.getInstance().currentController;
gameObject.AddComponent<DiskFactory>();
InitRuler();
}
// 初始化ruler
void InitRuler()
{
ruler.trialNum = 0;
ruler.roundNum = 0;
ruler.roundSum = 2;
ruler.sendTime = 0;
ruler.roundDisksNum = new int[10];
generateRoundDisksNum();
}
// 生成每trial同时发出的飞碟数量的数组,同时发出飞碟个数不超过4
public void generateRoundDisksNum()
{
for (int i = 0; i < 10; ++i)
{
ruler.roundDisksNum[i] = Random.Range(0, 4) + 1;
}
}
public void Reset()
{
InitRuler();
scoreRecorder.Reset();
}
public void Record(Disk disk)
{
scoreRecorder.Record(disk);
}
public int GetScores()
{
return scoreRecorder.score;
}
public void SetRoundSum(int roundSum)
{
ruler.roundSum = roundSum;
}
// 设置游戏模式,同时支持物理运动模式和动力学运动模式
public void SetPlayDiskMode(bool isPhysis)
{
if (isPhysis)
{
actionManager = Singleton<PhysisActionManager>.Instance as IActionManager;
}
else
{
actionManager = Singleton<CCActionManager>.Instance as IActionManager;
}
}
// 发射飞碟
public void LaunchDisk()
{
// 使飞碟飞入位置尽可能分开,从不同位置飞入使用的数组
int[] beginPosY = new int[5] { 0, 0, 0, 0, 0 };
for (int i = 0; i < ruler.roundDisksNum[ruler.trialNum]; ++i)
{
int random;
// 如果是第一轮,速度为2-4;否则是2-6
if (ruler.roundNum == 1)
random = (Random.Range(0, 2) + 1) * 2;
else
random = (Random.Range(0, 3) + 1) * 2;
ruler.speed = random;
// 飞碟颜色与大小
random = Random.Range(0, 3) + 1;
ruler.size = random;
if (random == 1)
{
ruler.color = "red";
}
else if (random == 2)
{
ruler.color = "green";
}
else
{
ruler.color = "blue";
}
// 飞碟飞入的方向
random = Random.Range(0, 2);
if (random == 1)
{
ruler.direction = new Vector3(3, 0.3f, 0);
}
else
{
ruler.direction = new Vector3(-3, 0.3f, 0);
}
// 确定不同飞碟的飞入位置
do
{
random = Random.Range(0, 4) + 1;
} while (beginPosY[random] != 0);
beginPosY[random] = 1;
ruler.beginPos = new Vector3(-ruler.direction.x * 4, 1.5f * random, 0);
// 根据ruler从工厂中生成一个飞碟
GameObject disk = Singleton<DiskFactory>.Instance.GetDisk(ruler);
// 设置飞碟的飞行动作
actionManager.PlayDisk(disk, ruler.speed, ruler.direction);
}
}
// 释放工厂飞碟
public void FreeFactoryDisk(GameObject disk)
{
Singleton<DiskFactory>.Instance.FreeDisk(disk);
}
// 释放所有工厂飞碟
public void FreeAllFactoryDisk()
{
GameObject[] obj = FindObjectsOfType(typeof(GameObject)) as GameObject[];
foreach (GameObject g in obj)
{
if (g.name == "Disk(Clone)")
{
Singleton<DiskFactory>.Instance.FreeDisk(g);
}
}
}
void Update()
{
if (mainController.GetGameState() == 1)
{
ruler.sendTime += Time.deltaTime;
// 每隔2s发送一次飞碟(trial)
if (ruler.sendTime > 2)
{
ruler.sendTime = 0;
// 当次数trial等于0时,说明进入下一个回合,回合加一
if (ruler.trialNum == 0)
ruler.roundNum++;
// 如果未到设定回合数
if (ruler.roundNum <= ruler.roundSum)
{
// 发射飞碟,次数trial增加
mainController.SetUserInterfaceTip("");
LaunchDisk();
ruler.trialNum++;
// 当次数trial等于10时,说明一个回合已经结束,重新生成飞碟数组
if (ruler.trialNum == 10)
{
ruler.trialNum = 0;
generateRoundDisksNum();
}
}
// 否则游戏结束,提示重新进行游戏
else
{
mainController.SetUserInterfaceTip("Click Restart and Play Again!");
mainController.SetGameState(2);
}
// 设置回合数和trial数目的提示
if(ruler.roundNum > ruler.roundSum)
mainController.SetUserInterfaceRoundNum(ruler.roundSum);
else
mainController.SetUserInterfaceRoundNum(ruler.roundNum);
mainController.SetUserInterfaceTrialNum(ruler.trialNum);
}
}
}
}
IActionManager
提供了PlayDisk的接口方法,分别由CCActionManager和PhysisActionManager实现。
using UnityEngine;
public interface IActionManager
{
void PlayDisk(GameObject disk, float speed, Vector3 direction);
}
ISSActionCallback
飞碟运动完成后的回调接口,当飞碟飞行完成之后,就会调用这个接口里的SSActionEvent方法,完成对飞碟对象的销毁,该方法分别由CCActionManager和PhysisActionManager实现。
using UnityEngine;
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);
}
CCActionManager和PhysisActionManager
提供了PlayDisk方法、SSActionEvent方法的实现。
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback, IActionManager
{
CCPlayDiskAction PlayDiskAction; // 飞碟空中动作
public void PlayDisk(GameObject disk, float speed, Vector3 direction)
{
PlayDiskAction = CCPlayDiskAction.GetSSAction(direction, speed);
RunAction(disk, PlayDiskAction, this);
}
// 回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
// 结束飞行后回收飞碟
Singleton<RoundController>.Instance.FreeFactoryDisk(source.gameObject);
}
}
using UnityEngine;
public class PhysisActionManager : SSActionManager, ISSActionCallback, IActionManager
{
PhysisPlayDiskAction PlayDiskAction; // 飞碟空中动作
public void PlayDisk(GameObject disk, float speed, Vector3 direction)
{
PlayDiskAction = PhysisPlayDiskAction.GetSSAction(direction, speed);
RunAction(disk, PlayDiskAction, this);
}
// 回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
// 结束飞行后回收飞碟
Singleton<RoundController>.Instance.FreeFactoryDisk(source.gameObject);
}
}
SSActionManager
是CCActionManager和PhysisActionManager的基类。
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class SSActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();
// Update is called once per frame
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()); // release action
}
else if (ac.enable)
{
ac.Update(); // update action
}
}
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();
}
}
SSAction
是CCPlayDiskAction和PhysisPlayDiskAction的基类。
using UnityEngine;
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameObject { get; set; } // 附着游戏对象
public Transform transform { get; set; } // 游戏对象的的运动
public ISSActionCallback callback { get; set; } // 回调函数
protected SSAction() {}
// Use this for initialization
public virtual void Start()
{
throw new System.NotImplementedException();
}
// Update is called once per frame
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
CCPlayDiskAction和PhysisPlayDiskAction
实现了运动学类型和物理类型动作的实现,重写了Start和Update。
using UnityEngine;
public class CCPlayDiskAction : SSAction
{
float gravity; // 垂直速度
float speed; // 水平速度
Vector3 direction; // 方向
float time; // 时间
public static CCPlayDiskAction GetSSAction(Vector3 direction, float speed)
{
CCPlayDiskAction action = ScriptableObject.CreateInstance<CCPlayDiskAction>();
action.gravity = 9.8f;
action.time = 0;
action.speed = speed;
action.direction = direction;
return action;
}
public override void Start()
{
gameObject.GetComponent<Rigidbody>().isKinematic = true;
}
public override void Update()
{
time += Time.deltaTime;
transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
transform.Translate(direction * speed * Time.deltaTime);
// 飞碟到达底部动作结束,回调
if (this.transform.position.y < -5)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
using UnityEngine;
public class PhysisPlayDiskAction : SSAction
{
float speed; // 水平速度
Vector3 direction; // 飞行方向
public static PhysisPlayDiskAction GetSSAction(Vector3 direction, float speed)
{
PhysisPlayDiskAction action = ScriptableObject.CreateInstance<PhysisPlayDiskAction>();
action.speed = speed;
action.direction = direction;
return action;
}
public override void Start()
{
gameObject.GetComponent<Rigidbody>().isKinematic = false;
// 水平初速度
gameObject.GetComponent<Rigidbody>().velocity = speed * direction;
}
public override void Update()
{
// 飞碟到达底部动作结束,回调
if (this.transform.position.y < -5)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
MainController
该类负责控制RoundController和UserInterface,每当RoundController监测到游戏事件发生,MainController就将状态改变传入UserInterface,使得游戏界面改变。
using UnityEngine;
using UnityEngine.SocialPlatforms.Impl;
public class MainController : MonoBehaviour
{
private RoundController roundController; // 回合控制器
private UserInterface userInterface;
private int gameRound; // 游戏回合
private int gameState; // 0为游戏准备开始,1为游戏正在进行,2为游戏结束
void Start()
{
SSDirector.getInstance().currentController = this;
roundController = gameObject.AddComponent<RoundController>();
userInterface = gameObject.AddComponent<UserInterface>();
gameState = 0;
gameRound = 2;
}
// 必要的Get函数
public int GetGameRound()
{
return gameRound;
}
public int GetGameState()
{
return gameState;
}
// 必要的Set函数
public void SetGameState(int state)
{
gameState = state;
}
public void SetRoundSum(int roundSum)
{
if (roundSum < 2)
return;
gameRound = roundSum;
roundController.SetRoundSum(roundSum);
}
// 与其他类相关的Get、Set函数
public void SetPlayDiskMode(bool isPhysis)
{
roundController.SetPlayDiskMode(isPhysis);
}
public void SetUserInterfaceTip(string tip)
{
userInterface.SetTip(tip);
}
public void SetUserInterfaceScore(int score)
{
userInterface.SetScore(score);
}
public void SetUserInterfaceRoundNum(int round)
{
userInterface.SetRoundNum(round);
}
public void SetUserInterfaceTrialNum(int trial)
{
userInterface.SetTrialNum(trial);
}
public int GetScore()
{
return roundController.GetScores();
}
public void Restart()
{
userInterface.Init();
roundController.Reset();
}
public void Hit(Vector3 position)
{
//Debug.Log("PRESS");
Camera camera = Camera.main;
Ray ray = camera.ScreenPointToRay(position);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if (hit.collider.gameObject.GetComponent<Disk>() != null)
{
//把击中的飞碟移出屏幕,触发回调释放
hit.collider.gameObject.transform.position = new Vector3(0, -6, 0);
// 记录飞碟得分
roundController.Record(hit.collider.gameObject.GetComponent<Disk>());
// 显示当前得分
userInterface.SetScore(roundController.GetScores());
}
}
}
// 释放所有工厂飞碟
public void FreeAllFactoryDisk()
{
roundController.FreeAllFactoryDisk();
}
void Update()
{
if(gameState == 1)
{
if (Input.GetButtonDown("Fire1"))
{
Debug.Log("PRESS");
Hit(Input.mousePosition);
userInterface.SetScore(GetScore());
}
}
}
}
UserInterface
该类用于显示游戏界面,包括按钮、标题等等。
using UnityEngine;
using System.Collections;
public class UserInterface : MonoBehaviour
{
private MainController mainController;
private int score;
private string tip;
private string roundNum;
private string trialNum;
void Start()
{
Init();
mainController = SSDirector.getInstance().currentController;
}
// 一些必要的set函数
public void SetTip(string tip)
{
this.tip = tip;
}
public void SetScore(int score)
{
this.score = score;
}
public void SetRoundNum(int round)
{
roundNum = "回合: " + round;
}
public void SetTrialNum(int trial)
{
if (trial == 0) trial = 10;
trialNum = "Trial: " + trial;
}
public void Init()
{
score = 0;
tip = "";
roundNum = "";
trialNum = "";
}
public void AddTitle()
{
GUIStyle titleStyle = new GUIStyle();
titleStyle.normal.textColor = Color.black;
titleStyle.fontSize = 50;
GUI.Label(new Rect(Screen.width / 2 - 80, 20, 60, 100), "Hit UFO", titleStyle);
}
public void ShowHomePage()
{
GUIStyle labelStyle = new GUIStyle();
labelStyle.normal.textColor = Color.black;
labelStyle.fontSize = 30;
GUI.Label(new Rect(450, 250, 100, 50), "回合: " + mainController.GetGameRound(), labelStyle);
if (GUI.Button(new Rect(420, 150, 160, 80), "开始游戏\n(默认为2回合)"))
{
mainController.Restart();
mainController.SetGameState(1);
}
if (GUI.Button(new Rect(420, 253, 30, 30), "+"))
{
Debug.Log(mainController.GetGameRound());
mainController.SetRoundSum(mainController.GetGameRound() + 1);
}
if(GUI.Button(new Rect(542, 253, 30, 30), "-"))
{
Debug.Log(mainController.GetGameRound());
mainController.SetRoundSum(mainController.GetGameRound() - 1);
}
}
public void ShowGamePage()
{
// 游戏信息显示
GUIStyle labelStyle = new GUIStyle();
labelStyle.normal.textColor = Color.black;
labelStyle.fontSize = 30;
GUI.Label(new Rect(750, 10, 100, 50), "得分: " + score, labelStyle);
GUI.Label(new Rect(310, 170, 50, 200), tip, labelStyle);
GUI.Label(new Rect(750, 60, 100, 50), roundNum, labelStyle);
GUI.Label(new Rect(750, 110, 100, 50), trialNum, labelStyle);
// 退出游戏
if (GUI.Button(new Rect(10, 10, 100, 50), "退出游戏"))
{
mainController.FreeAllFactoryDisk();
mainController.Restart();
mainController.SetGameState(0);
}
// 选择模式
if (GUI.Button(new Rect(10, Screen.height - 115, 110, 50), "运动学模式"))
{
mainController.FreeAllFactoryDisk();
mainController.SetPlayDiskMode(false);
mainController.Restart();
mainController.SetGameState(1);
}
if (GUI.Button(new Rect(10, Screen.height - 55, 110, 50), "物理模式"))
{
mainController.FreeAllFactoryDisk();
mainController.SetPlayDiskMode(true);
mainController.Restart();
mainController.SetGameState(1);
}
}
public void ShowRestart()
{
ShowGamePage();
if (GUI.Button(new Rect(440, 230, 100, 60), "Restart"))
{
mainController.FreeAllFactoryDisk();
mainController.Restart();
mainController.SetGameState(1);
}
}
public void Show()
{
// 游戏开始界面
if (mainController.GetGameState() == 0)
{
ShowHomePage();
}
else if (mainController.GetGameState() == 1)
{
ShowGamePage();
}
else
{
ShowRestart();
}
}
void OnGUI()
{
AddTitle();
Show();
}
}