用Unity3D实现打飞碟游戏
实验代码:传送门
效果图
UML图:
游戏规则
- 游戏有 n 个 round,每个 round 都包括10 次 trial。
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制。
- 每个 trial 的飞碟有随机性,总体难度随 round 上升。
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算。
实验要求
使用对象池管理飞碟对象
飞碟预制体的制作
新开一个空对象,在这个空对象中加入一个球,一个胶囊体对象。球为Edge,胶囊体为Body。其中,球的缩放为:1,0.25,1,胶囊体的缩放为:0.5,0.2,0.5。
代码编写
Models
DiskFactory.cs
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour
{
public GameObject diskPrefab; // 飞碟游戏对象,创建新的飞碟游戏对象的复制对象
private List<DiskData> used; // 正在被游戏使用的飞碟对象
private List<DiskData> free; // 没有被使用的空闲飞碟对象
public void Start()
{
diskPrefab = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
diskPrefab.SetActive(false);
used = new List<DiskData>();
free = new List<DiskData>();
}
// 飞碟获取方法,根据ruler获取相应飞碟
public GameObject GetDisk(Ruler ruler)
{
GameObject disk;
// 从缓存中获取飞碟,没有则先创建
int diskNum = free.Count;
if (diskNum == 0)
{
disk = GameObject.Instantiate(diskPrefab, Vector3.zero, Quaternion.identity);
disk.AddComponent(typeof(DiskData));
}
else
{
disk = free[diskNum - 1].gameObject;
free.Remove(free[diskNum - 1]);
}
// 根据ruler设置disk的速度、颜色、大小、飞入方向
disk.GetComponent<DiskData>().speed = ruler.speed;
disk.GetComponent<DiskData>().color = ruler.color;
disk.GetComponent<DiskData>().size = ruler.size;
// 给飞碟上颜色
if (ruler.color == "red")
{
foreach (var child in disk.GetComponentsInChildren<Transform>())
{
child.GetComponent<Renderer>().material.color = Color.red;
}
}
else if (ruler.color == "green")
{
foreach (var child in disk.GetComponentsInChildren<Transform>())
{
child.GetComponent<Renderer>().material.color = Color.green;
}
}
else
{
foreach (var child in disk.GetComponentsInChildren<Transform>())
{
child.GetComponent<Renderer>().material.color = Color.blue;
}
}
// 绘制飞碟大小
disk.transform.localScale = new Vector3((float)ruler.size,(float)ruler.size, (float)ruler.size);
// 选择飞碟飞入屏幕的起始位置
disk.transform.position = ruler.beginPos;
// 设置飞碟显示
disk.SetActive(true);
// 将飞碟加入使用队列
used.Add(disk.GetComponent<DiskData>());
return disk;
}
// 飞碟回收方法,将不使用的飞碟从使用队列放到空闲队列中
public void FreeDisk(GameObject disk)
{
foreach (DiskData d in used)
{
if (d.gameObject.GetInstanceID() == disk.GetInstanceID())
{
disk.SetActive(false);
used.Remove(d);
free.Add(d);
break;
}
}
}
}
在这段代码中,使用了对象池技术来管理飞碟对象的创建和回收。对象池是一种常见的优化技术,旨在避免频繁的创建和销毁对象,提高性能和内存利用率。以下是代码中使用的对象池技术的解释:
-
在Start()方法中,通过实例化一个飞碟对象的预制体,创建了一个初始的飞碟对象(diskPrefab)。这个对象会作为对象池的原型,用于后续创建新的飞碟对象。
-
代码中使用两个列表(used和free)来管理飞碟对象。used列表存储正在被游戏使用的飞碟对象,而free列表存储没有被使用的空闲飞碟对象。
-
在GetDisk()方法中,首先检查free列表中是否有空闲的飞碟对象。如果没有,则通过对象池的机制,使用Instantiate()方法创建一个新的飞碟对象实例,并添加DiskData组件用于存储飞碟的属性信息。
-
如果free列表中有空闲的飞碟对象,则从列表中取出最后一个对象,并将其从free列表中移除。这样可以避免频繁地创建新的飞碟对象,而是复用之前创建的对象。
-
在FreeDisk()方法中,当飞碟对象不再使用时,将其设为非激活状态,并将其从used列表中移除。然后将该飞碟对象添加到free列表中,以便在下次需要时可以直接从free列表中获取。
通过使用对象池技术,可以减少频繁的对象创建和销毁操作,从而提高性能。通过复用已有的对象,可以减少内存分配的开销,并避免垃圾回收的频繁发生,进而提高游戏的性能和流畅度。
DiskData.cs
using UnityEngine;
public class DiskData : MonoBehaviour
{
public int size; // 大小
public string color; // 颜色
public int speed; // 发射速度
}
View
view.cs
using UnityEngine;
public class View : MonoBehaviour
{
private FirstController mainController;
private int score;
private string tip;
private string roundNum;
private string trialNum;
public GUISkin gameSkin; // 游戏控件的皮肤风格
void Start()
{
score = 0;
tip = "";
roundNum = "";
trialNum = "";
mainController = SSDirector.GetInstance().currentController;
}
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), "打飞碟", titleStyle);
}
public void AddChooseModeButton()
{
GUI.skin = gameSkin;
if (GUI.Button(new Rect(Screen.width / 2 - 80 , 100, 160, 80), "普通模式\n(默认为" + mainController.GetN() + "回合)"))
{
mainController.SetRoundSum(mainController.GetN());
mainController.Restart();
mainController.SetGameState((int)GameState.Playing);
}
if (GUI.Button(new Rect(Screen.width / 2 - 80 , 210, 160, 80), "无尽模式\n(回合数无限)"))
{
mainController.SetRoundSum(-1);
mainController.Restart();
mainController.SetGameState((int)GameState.Playing);
}
}
public void ShowHomePage()
{
AddChooseModeButton();
}
public void AddActionModeButton()
{
GUI.skin = gameSkin;
if (GUI.Button(new Rect(10, Screen.height - 100, 110, 40), "运动学模式"))
{
mainController.FreeAllFactoryDisk();
mainController.SetPlayDiskModeToPhysis(false);
}
if (GUI.Button(new Rect(10, Screen.height - 50, 110, 40), "物理模式(飞碟直接会又碰撞)"))
{
mainController.FreeAllFactoryDisk();
mainController.SetPlayDiskModeToPhysis(true);
}
}
public void AddBackButton()
{
GUI.skin = gameSkin;
if (GUI.Button(new Rect(10, 10, 60, 40), "Back"))
{
mainController.FreeAllFactoryDisk();
mainController.Restart();
mainController.SetGameState((int)GameState.Ready);
}
}
public void AddGameLabel()
{
GUIStyle labelStyle = new GUIStyle();
labelStyle.normal.textColor = Color.black;
labelStyle.fontSize = 30;
GUI.Label(new Rect(Screen.width - 160, 10, 100, 50), "得分: " + score, labelStyle);
GUI.Label(new Rect(Screen.width / 2 - 200, 80, 50, 200), tip, labelStyle);
GUI.Label(new Rect(Screen.width - 160, 60, 100, 50), roundNum, labelStyle);
GUI.Label(new Rect(Screen.width - 160, 110, 100, 50), trialNum, labelStyle);
}
public void AddRestartButton()
{
if (GUI.Button(new Rect(Screen.width / 2 - 80 , 150, 100, 60), "Restart"))
{
mainController.FreeAllFactoryDisk();
mainController.Restart();
mainController.SetGameState((int)GameState.Playing);
}
}
public void ShowGamePage()
{
AddGameLabel();
AddBackButton();
AddActionModeButton();
if (Input.GetButtonDown("Fire1"))
{
mainController.Hit(Input.mousePosition);
}
}
public void ShowRestart()
{
ShowGamePage();
AddRestartButton();
}
void OnGUI()
{
AddTitle();
mainController.ShowPage();
}
}
Control
SSDirector.cs
public class SSDirector : System.Object
{
private static SSDirector instance;
public FirstController currentController { get; set; }
public static SSDirector GetInstance()
{
if (instance == null)
{
instance = new SSDirector();
}
return instance;
}
}
Singleton.cs
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;
}
}
}
Ruler.cs
// 飞碟获取规则
using UnityEngine;
public struct Ruler
{
public int trialNum; // 当前trial的编号
public int roundNum; // 当前round的编号
public int roundSum; // 一共round的总数目
public int[] roundDisksNum; // 每一轮对于trial的飞碟数量
public float sendTime; // 发射间隔时间
public int size; // 飞碟大小
public int speed; // 飞碟速度
public string color; // 飞碟颜色
public Vector3 direction; // 飞碟飞入方向
public Vector3 beginPos; // 飞碟飞入位置
};
FirstController.cs
/* 游戏状态,0为准备进行,1为正在进行游戏,2为结束 */
using UnityEngine;
enum GameState
{
Ready = 0, Playing = 1, GameOver = 2
};
public class FirstController : MonoBehaviour
{
private RoundController roundController; // 回合控制器
private View view; // 游戏视图
private int N; // 默认游戏回合
private int gameState;
public GUISkin gameSkin;
void Awake()
{
SSDirector.GetInstance().currentController = this;
roundController = gameObject.AddComponent<RoundController>();
view = gameObject.AddComponent<View>();
gameState = (int)GameState.Ready;
view.gameSkin = gameSkin;
N = 2;
}
public int GetN()
{
return N;
}
public void Restart()
{
view.Init();
roundController.Reset();
}
public void SetGameState(int state)
{
gameState = state;
}
public int GetGameState()
{
return gameState;
}
public void ShowPage()
{
switch (gameState)
{
case 0:
view.ShowHomePage();
break;
case 1:
view.ShowGamePage();
break;
case 2:
view.ShowRestart();
break;
}
}
public void SetRoundSum(int roundSum)
{
roundController.SetRoundSum(roundSum);
}
public void SetPlayDiskModeToPhysis(bool isPhysis)
{
roundController.SetPlayDiskModeToPhysis(isPhysis);
}
public void SetViewTip(string tip)
{
view.SetTip(tip);
}
public void SetViewScore(int score)
{
view.SetScore(score);
}
public void SetViewRoundNum(int round)
{
view.SetRoundNum(round);
}
public void SetViewTrialNum(int trial)
{
view.SetTrialNum(trial);
}
public void Hit(Vector3 position)
{
Camera camera = Camera.main;
Ray ray = camera.ScreenPointToRay(position);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
foreach (RaycastHit hit in hits)
{
if (hit.collider.transform.parent.GetComponent<DiskData>() != null)
{
// 把击中的飞碟移出屏幕,触发回调释放
hit.collider.transform.parent.transform.position = new Vector3(0, -6, 0);
// 记录飞碟得分
roundController.Record(hit.collider.transform.parent.GetComponent<DiskData>());
// 显示当前得分
view.SetScore(roundController.GetScores());
}
}
}
// 释放所有工厂飞碟
public void FreeAllFactoryDisk()
{
roundController.FreeAllFactoryDisk();
}
}
RoundController.cs
using System.Data;
using UnityEngine;
public class RoundController : MonoBehaviour
{
private IActionManager actionManager; // 选择飞碟的运动类型
private ScoreRecorder scoreRecorder; // 记分器
private FirstController firstController;
private Ruler ruler; // 飞碟获取规则
void Start()
{
// 一开始飞碟的运动类型默认为运动学运动
actionManager = gameObject.AddComponent<CCActionManager>();
gameObject.AddComponent<PhysisActionManager>();
scoreRecorder = new ScoreRecorder();
firstController = SSDirector.GetInstance().currentController;
gameObject.AddComponent<DiskFactory>();
InitRuler();
}
void InitRuler()
{
ruler.trialNum = 0;
ruler.roundNum = 0;
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(DiskData disk)
{
scoreRecorder.Record(disk);
}
public int GetScores()
{
return scoreRecorder.score;
}
public void SetRoundSum(int roundSum)
{
ruler.roundSum = roundSum;
}
// 设置游戏模式,同时支持物理运动模式和动力学运动模式
public void SetPlayDiskModeToPhysis(bool isPhysis)
{
if (isPhysis)
{
actionManager = Singleton<PhysisActionManager>.Instance as IActionManager;
}
else
{
actionManager = Singleton<CCActionManager>.Instance as IActionManager;
}
}
// 发射飞碟
public void LaunchDisk()
{
// 使飞碟飞入位置尽可能分开,从不同位置飞入使用的数组
int[] beginPosY = new int[4] { 0, 0, 0, 0 };
for (int i = 0; i < ruler.roundDisksNum[ruler.trialNum]; ++i)
{
// 获取随机数
int randomNum = Random.Range(0, 3) + 1;
// 飞碟速度随回合数增加而变快,这样难度增加
ruler.speed = randomNum * (ruler.roundNum + 4);
// 重新选取随机数,并根据随机数选择飞碟颜色
randomNum = Random.Range(0, 3) + 1;
if (randomNum == 1)
{
ruler.color = "red";
}
else if (randomNum == 2)
{
ruler.color = "green";
}
else
{
ruler.color = "blue";
}
// 重新选取随机数,并根据随机数选择飞碟的大小
ruler.size = Random.Range(0, 3) + 1;
// 重新选取随机数,并根据随机数选择飞碟飞入的方向
randomNum = Random.Range(0, 2);
if (randomNum == 1)
{
ruler.direction = new Vector3(3, 1, 0);
}
else
{
ruler.direction = new Vector3(-3, 1, 0);
}
// 重新选取随机数,并使不同飞碟的飞入位置尽可能分开
do
{
randomNum = Random.Range(0, 4);
} while (beginPosY[randomNum] != 0);
beginPosY[randomNum] = 1;
ruler.beginPos = new Vector3(-ruler.direction.x * 4, -0.5f * randomNum, 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.gameObject.name == "Disk(Clone)(Clone)")
{
Singleton<DiskFactory>.Instance.FreeDisk(g);
}
}
}
void Update()
{
if (firstController.GetGameState() == (int)GameState.Playing)
{
ruler.sendTime += Time.deltaTime;
// 每隔2s发送一次飞碟(trial)
if (ruler.sendTime > 2)
{
ruler.sendTime = 0;
// 如果为无限回合或还未到设定回合数
if (ruler.roundSum == -1 || ruler.roundNum < ruler.roundSum)
{
// 发射飞碟,次数trial增加
firstController.SetViewTip("");
LaunchDisk();
ruler.trialNum++;
// 当次数trial等于10时,说明一个回合已经结束,回合加一,重新生成飞碟数组
if (ruler.trialNum == 10)
{
ruler.trialNum = 0;
ruler.roundNum++;
generateRoundDisksNum();
}
}
// 否则游戏结束,提示重新进行游戏
else
{
firstController.SetViewTip("Click Restart and Play Again!");
firstController.SetGameState((int)GameState.GameOver);
}
// 设置回合数和trial数目的提示
if (ruler.trialNum == 0) firstController.SetViewRoundNum(ruler.roundNum);
else firstController.SetViewRoundNum(ruler.roundNum + 1);
firstController.SetViewTrialNum(ruler.trialNum);
}
}
}
}
ScoreRecorder.cs
public class ScoreRecorder
{
public int score; // 游戏分数
public ScoreRecorder()
{
score = 0;
}
/* 记录分数,根据点击中的飞碟的大小,速度,颜色计算得分 */
public void Record(DiskData disk)
{
// 飞碟越小分就越高,大小为1得3分,大小为2得2分,大小为3得1分
int diskSize = disk.size;
switch (diskSize)
{
case 1:
score += 3;
break;
case 2:
score += 2;
break;
case 3:
score += 1;
break;
default: break;
}
// 速度越快分就越高
score += disk.speed;
// 颜色为红色得1分,颜色为黄色得2分,颜色为蓝色得3分
string diskColor = disk.color;
if (diskColor.CompareTo("red") == 0)
{
score += 1;
}
else if (diskColor.CompareTo("green") == 0)
{
score += 2;
}
else if (diskColor.CompareTo("blue") == 0)
{
score += 3;
}
}
/* 重置分数,设为0 */
public void Reset()
{
score = 0;
}
}
Action
CCActionManager.cs
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);
}
}
CCPlayDiskAction.cs
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);
}
}
}
IActionManager.cs
using UnityEngine;
public interface IActionManager
{
void PlayDisk(GameObject disk, float speed, Vector3 direction);
}
ISSActionCallback.cs
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);
}
PhysisActionManager.cs
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);
}
}
PhysisPlayDiskAction.cs
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);
}
}
}
SSAction.cs
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; } // 回调函数
public virtual void Start() { } // Start()重写方法
public virtual void Update() { } // Update()重写方法
}
SSActionManager.cs
using System.Collections.Generic;
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>();
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);
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();
}
}