前言
在打飞碟游戏中,每一轮会产生若干飞碟随机飞入,不同的飞碟带有不同的分数,玩家需要用鼠标点击飞行中的飞碟来实现打飞碟的行为,打中即可得分,总共有四轮,四轮后游戏结束,玩家需要得到尽可能高分。
游戏对象(飞碟)的预制制作
创建一个Cylinder类型对象,设置高度y=0.1,添加组件Rigidbody2D和Capsule Collider 2D
对象池
对象池(Object Pool)是一种设计模式,用于管理和重用一组预先创建的对象,而不是在每次需要时都创建和销毁它们。这种模式的主要目的是减少创建和销毁对象的开销,特别是在频繁创建和销毁大量临时对象的情况下,可以显著提高性能和资源利用率。
对象池的基本概念
-
预创建对象:在程序启动时或某个初始化阶段,预先创建一批对象,并将它们存储在一个集合中(通常是列表或队列)。这些对象处于未使用状态,等待被分配。
-
对象借用:当需要使用一个对象时,不是直接创建新的对象,而是从对象池中“借”一个已存在的对象。这通常涉及到从集合中取出一个对象,并将其标记为正在使用。
-
对象归还:当对象不再需要时,不是直接销毁它,而是将其“归还”到对象池中。这通常涉及将对象重置到初始状态,并将其放回集合中,以便下次再被借用。
-
对象管理:对象池还需要处理一些管理任务,例如:
- 对象数量控制:确保对象池中对象的数量在一定范围内,防止过多对象占用内存。
- 对象状态检查:确保归还的对象处于有效状态,可以被再次借用。
- 对象销毁:在必要时销毁不再需要的对象,释放资源。
// 定义一个对象池类
public class ObjectPool
{
private List<GameObject> pool; // 对象池列表
private GameObject prefab; // 对象的预制体
private int poolSize; // 对象池的大小
// 构造函数,初始化对象池
public ObjectPool(GameObject prefab, int poolSize)
{
this.prefab = prefab;
this.poolSize = poolSize;
pool = new List<GameObject>();
// 在对象池中创建指定数量的对象
for (int i = 0; i < poolSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Add(obj);
}
}
// 从对象池中获取一个对象
public GameObject GetObject()
{
// 遍历对象池,找到一个未激活的对象并返回
foreach (GameObject obj in pool)
{
if (!obj.activeInHierarchy)
{
obj.SetActive(true);
return obj;
}
}
// 如果没有未激活的对象,创建一个新的对象并返回
GameObject newObj = Instantiate(prefab);
newObj.SetActive(true);
pool.Add(newObj);
return newObj;
}
// 将对象放回对象池
public void ReturnObject(GameObject obj)
{
obj.SetActive(false);
}
}
// 使用对象池
public class ObjectPoolExample : MonoBehaviour
{
public GameObject prefab; // 预制体
public int poolSize; // 对象池的大小
private ObjectPool objectPool; // 对象池实例
private void Start()
{
objectPool = new ObjectPool(prefab, poolSize);
}
private void Update()
{
// 按下空格键从对象池中获取一个对象,并将其放置在鼠标点击的位置
if (Input.GetKeyDown(KeyCode.Space))
{
GameObject obj = objectPool.GetObject();
obj.transform.position = Input.mousePosition;
}
}
}
UML设计图
代码实现
设计本游戏时,我采用了MVC架构,MVC(Model-View-Controller)架构是一种软件设计模式,广泛应用于用户界面(UI)的设计和开发中。MVC 架构将应用程序分为三个主要组件:模型(Model)、视图(View)和控制器(Controller)。
一、Action动作和动作管理器
SSActionManager类
Update()方法:
添加动作:将watinglist列表的动作添加到actions字典
更新动作:检查每个action的destory和enable状态,进行摧毁或更新
删除动作:遍历watinglist列表,从actions字典移除并销毁
RunAction()方法:
启用一个新动作,设置动作属性,添加到等待列表并启用。
RemainActionCoun()方法:
返回当前actions字典的动作数量
using System.Collections;
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> ();
// 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.destory) {
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 ();
}
// Use this for initialization
protected void Start () {
}
public int RemainActionCount()
{
return actions.Count;
}
}
CCActionManager类
SSActionEvent()方法:
当action完成时,调用DiskFactory的FreeDisk回收飞碟
MoveDisk()方法:
创建一个CCFlyAction示例,传入飞碟速度,使用RunAction方法启动该动作
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback {
private FirstController sceneController;
//private CCMoveToAction moveToA , moveToB, moveToC, moveToD;
public CCFlyAction action;
public DiskFactory factory;
protected new void Start() {
sceneController = (FirstController)SSDirector.getInstance().currentSceneController;
sceneController.actionManager = this;
factory = Singleton<DiskFactory>.Instance;
}
// Update is called once per frame
// protected new void Update ()
// {
// base.Update ();
// }
#region ISSActionCallback implementation
public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
{
factory.FreeDisk(source.transform.gameObject);
}
#endregion
public void MoveDisk(GameObject disk){
action = CCFlyAction.GetSSAction(disk.GetComponent<DiskAttributes>().speedX,disk.GetComponent<DiskAttributes>().speedY);
RunAction(disk,action,this);
}
}
CCFlyAction类
GetSSAction()方法:
创建一个CCFlyAction实例,并设置速度然后返回该实例
Start()方法
在动作开始时调用,配置飞碟的物理属性和初始速度
Update()方法:
检查飞碟的状态并处理出界和销毁的情况。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCFlyAction : SSAction
{
public float speedX;
public float speedY;
public static CCFlyAction GetSSAction(float x, float y){
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction> ();
action.speedX = x;
action.speedY = y;
return action;
}
// Start is called before the first frame update
public override void Start()
{
Rigidbody2D rb = this.transform.gameObject.GetComponent<Rigidbody2D>();
rb.velocity = new Vector2(speedX, speedY);
rb.gravityScale = 0f;
Vector2 force = new Vector2(0, -8);
rb.AddForce(force, ForceMode2D.Impulse);
}
// Update is called once per frame
public override void Update()
{
if(this.transform.gameObject.activeSelf == false){
// 如果飞碟已经被销毁
Debug.Log("飞碟已经销毁");
this.destory = true;
this.callback.SSActionEvent(this);
return;
}
Vector3 vec3 = Camera.main.WorldToScreenPoint(this.transform.position);
// 飞碟出界
if(vec3.x <-100 || vec3.x>Camera.main.pixelWidth+100 || vec3.y<-100 || vec3.y>Camera.main.pixelHeight+100){
Debug.Log("飞碟出界");
this.destory= true;
this.callback.SSActionEvent(this);
return;
}
}
}
二、Model类
Singleton类
该类用于单实例化飞碟工厂,使用了对象池的思想,通过使用Singleton模式,可以避免多个实例的创建和资源的浪费,同时也能确保实例的唯一性和数据的一致性。
using System.Collections;
using System.Collections.Generic;
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("instance null");
}
}
return instance;
}
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
DiskFactory类
该类用于管理飞碟对象,通过建立used和free两个列表来存储飞碟对象进行管理
GetDisk()方法:
获取一个飞碟对象,用于在游戏中生成新的飞碟
如果free列表中有可用的飞碟对象,从free列表中取出一个,如果free列表为空,使用Instantiate方法创建一个新的飞碟对象,并为其添加DiskAttributes组件,设置飞碟的随机旋转角度与DiskAttributes属性,生成随机分数,按照分数设定速度、颜色、大小,生成随机飞入方向,启用飞碟对象。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskAttributes : MonoBehaviour
{
public int score;
public float speedX;
public float speedY;
}
public class DiskFactory : MonoBehaviour
{
List<GameObject> used;
List<GameObject> free;
System.Random rand;
// Start is called before the first frame update
void Start()
{
used = new List<GameObject>();
free = new List<GameObject>();
rand = new System.Random();
}
// Update is called once per frame
void Update()
{
}
public GameObject GetDisk(int round){
GameObject disk;
if(free.Count != 0){
disk = free[0];
free.Remove(disk);
}
else{
// 调用Instantiate方法来生成一个disk的复制
disk = GameObject.Instantiate(Resources.Load("Prefabs/disk",typeof(GameObject))) as GameObject;
disk.AddComponent<DiskAttributes>();
}
// 设置一个随机方向
disk.transform.localEulerAngles = new Vector3(-rand.Next(20,40),0,0);
// GetComponent方法是获取一个指定的组件,例如Rigidbody组件
DiskAttributes attri = disk.GetComponent<DiskAttributes>();
// 该飞碟的分数随机1-4
attri.score = rand.Next(1,4);
// 分数决定飞碟的速度颜色大小
attri.speedX = (rand.Next(20,30) + attri.score + round)*0.2f;
attri.speedY = (rand.Next(5,10) + attri.score + round)*0.2f;
if(attri.score == 1){
disk.GetComponent<Renderer>().material.color = Color.blue;
}
else if(attri.score == 2){
disk.GetComponent<Renderer>().material.color = Color.green;
disk.transform.localScale += new Vector3(-0.2f,0,-0.2f);
}
else if(attri.score == 3){
disk.GetComponent<Renderer>().material.color = Color.red;
disk.transform.localScale += new Vector3(-0.5f,0,-0.5f);
}
// 飞碟的飞入方向
int direction = rand.Next(1,5);
// 左上,左下,右上,右下
if(direction == 1){
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 1.5f, 8)));
attri.speedY *= 0;
}
else if (direction == 2) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 0f, 8)));
attri.speedY =rand.Next(10,15);
}
else if (direction == 3) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 1.5f, 8)));
attri.speedX *= -1;
attri.speedY =0 ;
}
else if (direction == 4) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 0f, 8)));
attri.speedX *= -1;
attri.speedY =rand.Next(10,15);
}
used.Add(disk);
disk.SetActive(true);
return disk;
}
public void FreeDisk(GameObject disk){
disk.SetActive(false);
// 速度与大小恢复到预制
disk.transform.position = new Vector3(0,0,0);
disk.transform.localScale = new Vector3(2f,0.1f,2f);
used.Remove(disk);
free.Add(disk);
}
}
三、Controller控制类
ScoreController裁判类
ScoreController是一个控制计算得分的类
Record()方法:
记录飞碟的得分并更新UI界面
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreController : MonoBehaviour
{
int score;
public FirstController firstController;
public UserGUI userGUI;
// Start is called before the first frame update
void Start()
{
firstController = (FirstController)SSDirector.getInstance().currentSceneController;
firstController.scoreController = this;
userGUI = this.gameObject.GetComponent<UserGUI>();
}
// Update is called once per frame
void Update()
{
}
public void Record(GameObject disk){
score += disk.GetComponent<DiskAttributes>().score;
userGUI.score = score;
}
}
FirstController类
Awake()方法:
为当前游戏对象添加UserGUI,CCActionManager, ScoreController和DiskFactory组件。
GameOver()方法:
判断游戏是否结束,返回布尔值
Update()方法:
调用Gethit方法处理玩家射击,如果游戏结束,直接返回,否则,减少 timer
,如果 timer
小于等于 0 且没有剩余动作,生成 20 个飞碟并增加轮次,更新 userGUI
的轮次显示,重置 timer
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
int round = 0;
int max_round = 4;
float timer = 0.5f;
GameObject disk;
DiskFactory diskFactory;
public CCActionManager actionManager;
//public GameObject move1,move2;
public ScoreController scoreController;
public UserGUI userGUI;
// the first scripts
void Awake () {
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
director.currentSceneController.LoadResources();
gameObject.AddComponent<UserGUI>();
gameObject.AddComponent<CCActionManager>();
gameObject.AddComponent<ScoreController>();
gameObject.AddComponent<DiskFactory>();
diskFactory = Singleton<DiskFactory>.Instance;
userGUI = gameObject.GetComponent<UserGUI>();
}
// loading resources for first scence
public void LoadResources () {
;
}
public void Pause ()
{
throw new System.NotImplementedException ();
}
public void Resume ()
{
throw new System.NotImplementedException ();
}
#region IUserAction implementation
public bool GameOver(){
if(round >= max_round && actionManager.RemainActionCount()==0){
userGUI.gameMessage = "Game Over";
return true;
}
return false;
}
#endregion
// Use this for initialization
void Start () {
//give advice first
}
// Update is called once per frame
void Update () {
//give advice first
if (userGUI.mode == 0) return;
GetHit ();
if(GameOver()){
return;
}
timer -= Time.deltaTime;
if (timer <= 0 && actionManager.RemainActionCount()==0){
for(int i=0; i<20; i++){
disk = diskFactory.GetDisk(round);
actionManager.MoveDisk(disk);
}
round += 1;
if(round <= max_round){
userGUI.round = round;
}
timer=4.0f;
}
}
public void GetHit(){
if(Input.GetButtonDown("Fire1")){
Camera ca = Camera.main;
// 把鼠标左键的点击转换为一条从摄像机视角射出的射线
Ray ray = ca.ScreenPointToRay(Input.mousePosition);
// 判断射线碰撞到的物体
RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction);
if(hit.collider != null){
scoreController.Record(hit.transform.gameObject);
// 将命中的物体销毁
hit.transform.gameObject.SetActive(false);
}
}
}
}
四、View
为游戏绘制了基本页面,mode=0时,是游戏开始前的界面,玩家可以点击开始游戏按钮开始,mode=1时,是游戏界面,会有计分标签等部件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
public int mode;
public int score;
public int round;
public string gameMessage;
public GUIStyle bigStyle, smallStyle;
private int menu_width = Screen.width / 5, menu_height = Screen.width/10;
// Start is called before the first frame update
void Start()
{
mode = 0;
gameMessage = "";
bigStyle = new GUIStyle();
bigStyle.normal.textColor = Color.white;
bigStyle.normal.background = null;
bigStyle.alignment = TextAnchor.MiddleCenter;
bigStyle.fontSize = 50;
smallStyle = new GUIStyle();
smallStyle.normal.textColor = Color.white;
smallStyle.alignment = TextAnchor.MiddleCenter;
smallStyle.fontSize = 30;
smallStyle.normal.background = null;
}
// Update is called once per frame
void Update()
{
}
void OnGUI(){
GUI.skin.button.fontSize = 35;
switch(mode){
case 0:
mainMenu();
break;
case 1:
GameStart();
break;
}
}
void mainMenu(){
GUI.Label(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 0.1f, menu_width, menu_height), "打飞碟", bigStyle);
bool button = GUI.Button(new Rect(Screen.width/2 - menu_width*0.5f, Screen.height*0.4f, menu_width, menu_height),"开始游戏");
if(button){
mode = 1;
}
}
void GameStart(){
GUI.Label(new Rect(300, 60, 50, 200), gameMessage,bigStyle);
GUI.Label(new Rect(0, 0, 100, 50), "Score: " + score, smallStyle);
GUI.Label(new Rect(560, 0, 100, 50), "Round: " + round, smallStyle);
}
}
结语
感谢学长/学姐博客参考:参考博客