Unity實作打飛碟小游戲
專案地址
演示視頻
配接器模式概述
定義
配接器模式將某個類的介面轉換成客戶端期望的另一個介面表示,主的目的是兼容性,讓原本因介面不匹配不能一起作業的兩個類可以協同作業,其別名為包裝器(Wrapper),屬于結構型模式,
為表述方便,定義:
- 需要被適配的類、介面、物件,簡稱 src(source)
- 最終需要的輸出,簡稱 dst (destination,即Target)
- 配接器稱之為 Adapter ,
一句話描述配接器模式的感覺: src->Adapter->dst,即src以某種形式(三種形式分別對應三種配接器模式)給到Adapter里,最終轉化成了dst,
使用場景
- 系統需要使用現有的類,而這些類的介面不符合系統的需要,
- 想要建立一個可以重復使用的類,用于與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起作業,
- 需要一個統一的輸出介面,而輸入端的型別不可預知,
分類
- 類配接器,以類給到,在Adapter里,就是將src當做類,繼承,
- 物件配接器,以物件給到,在Adapter里,將src作為一個物件,持有,
- 介面配接器,以介面給到,在Adapter里,將src作為一個介面,實作,
專案要求
撰寫一個簡單的滑鼠打飛碟(Hit UFO)游戲
- 游戲內容要求:
- 游戲有 n 個 round,每個 round 都包括10 次 trial;
- 每個 trial 的飛碟的色彩、大小、發射位置、速度、角度、同時出現的個數都可能不同,它們由該 round 的 ruler 控制;
- 每個 trial 的飛碟有隨機性,總體難度隨 round 上升;
- 滑鼠點中得分,得分規則按色彩、大小、速度不同計算,規則可自由設定,
- 基礎版游戲的要求:
- 使用帶快取的工廠模式管理不同飛碟的生產與回收,該工廠必須是場景單實體的!具體實作見參考資源 Singleton 模板類
- 盡可能使用前面 MVC 結構實作人機互動與游戲模型分離
- 配接器版游戲的要求:
- 按 adapter模式 設計圖修改飛碟游戲
- 使它同時支持物理運動與運動學(變換)運動
專案配置
- 新建專案,將Assets檔案替換為我專案中的Assets檔案,由于第五章、第六章作業合并,故基礎版和運動與物理兼容版檔案夾下各有一個Assets
- 將Assets/Resources/Fantasy Skybox FREE/Materials/Classic中的第一個天空盒FS000_Day_01拖到Scene里
- 將Assets/Resources/Scripts中的DiskFactory、RoundController、ScoreRecorder拖到Main Camera上
- 編譯運行,開始游戲
核心演算法分析
基礎版(僅使用射線,動作管理)
專案框架如下:

DiskData.cs規定了飛碟有如下屬性:
public int score = 1; //射擊此飛碟得分
public Color color = Color.white; //飛碟顏色
public Vector3 direction; //飛碟初始的位置
public Vector3 scale = new Vector3( 1 ,0.25f, 1); //飛碟大小
MyDiskEditor.cs是用來制作預制的,實作飛碟屬性圖形化界面編輯:

具體代碼如下:
using UnityEngine;
using UnityEditor;
using System.Collections;
[CustomEditor(typeof(DiskData))]
[CanEditMultipleObjects]
public class MyDEditor : Editor
{
SerializedProperty score; //分數
SerializedProperty color; //顏色
SerializedProperty scale; //大小
void OnEnable()
{
//序列化物件后獲得各個值
score = serializedObject.FindProperty("score");
color = serializedObject.FindProperty("color");
scale = serializedObject.FindProperty("scale");
}
public override void OnInspectorGUI()
{
//開啟更新
serializedObject.Update();
//設定滑動條
EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score"));
if (!score.hasMultipleDifferentValues)
{
//顯示進度條
ProgressBar(score.intValue / 5f, "score");
}
//顯示值
EditorGUILayout.PropertyField(color);
EditorGUILayout.PropertyField(scale);
//應用更新
serializedObject.ApplyModifiedProperties();
}
private void ProgressBar(float value, string label)
{
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, value, label);
//中間留一個空行
EditorGUILayout.Space();
}
}
RoundController.cs是MVC結構里的控制器,是場景控制器,有以下幾個成員變數
public FlyActionManager action_manager;
public DiskFactory disk_factory;
public UserGUI user_gui;
public ScoreRecorder score_recorder;
private Queue<GameObject> disk_queue = new Queue<GameObject>(); //游戲場景中的飛碟佇列
private List<GameObject> disk_notshot = new List<GameObject>(); //沒有被打中的飛碟佇列
private int round = 1; //回合
private int trial = 1;
private float interval = 2.1f; //發射一個飛碟的時間間隔
private bool playing_game = false; //游戲中
private bool game_over = false; //游戲結束
private bool game_start = false; //游戲開始
實作了以下幾個方法
//初始化
void Start ();
//每幀更新,定時呼叫LoadResources函式通知飛碟加工廠生產飛碟,呼叫發射飛碟函式
void Update ();
//更新trial和round和生成飛碟間隔
public void UpdateTrial();
//通知飛碟加工廠生產飛碟并加入飛碟佇列
public void LoadResources();
//發射飛碟
private void SendDisk();
//處理點擊事件
public void Hit(Vector3 pos);
//獲得分數
public int GetScore();
//獲得round
public int GetRound();
//獲得trial
public int GetTrail();
//重新開始
public void ReStart();
//設定游戲結束
public void GameOver();
//暫停幾秒后回收飛碟
IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj);
//開始游戲
public void BeginGame();
一些關鍵函式實作如下
//每幀更新,定時呼叫LoadResources函式通知飛碟加工廠生產飛碟,呼叫發送飛碟函式
void Update ()
{
if(game_start)
{
//游戲結束,取消定時發送飛碟
if (game_over)
{
CancelInvoke("LoadResources");//取消呼叫LoadResources
}
//設定一個定時器,發送飛碟,游戲開始
if (!playing_game)
{
InvokeRepeating("LoadResources", 1f, interval);//1秒后呼叫LoadResources,每speed秒呼叫一次
playing_game = true;
}
//發射飛碟
SendDisk();
}
}
//發射飛碟
private void SendDisk()
{
float position_x = 9;
if (disk_queue.Count != 0)
{
GameObject disk = disk_queue.Dequeue();
disk_notshot.Add(disk);
disk.SetActive(true);
//設定被隱藏了或是新建的飛碟的位置
float ran_y = Random.Range(3f, 6f);
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk.GetComponent<DiskData>().direction = new Vector3(ran_x, ran_y, 0);
Vector3 position = new Vector3(-disk.GetComponent<DiskData>().direction.x * position_x, ran_y, 0);
disk.transform.position = position;
//設定飛碟初始所受的力和角度,飛碟速度總體隨round增大而加快,
float power = Random.Range(0.9f + 0.1f * round, 1.35f + 0.15f * round);
float angle = Random.Range(15f, 28f);
action_manager.UFOFly(disk,angle,power,round);
}
for (int i = 0; i < disk_notshot.Count; i++)
{
GameObject temp = disk_notshot[i];
//飛碟飛出攝像機視野也沒被打中
if (temp.transform.position.y < -3 && temp.gameObject.activeSelf == true)
{
if(user_gui.life>1)UpdateTrial();
disk_factory.FreeDisk(disk_notshot[i]);
disk_notshot.Remove(disk_notshot[i]);
//玩家血量-1
user_gui.ReduceBlood();
}
}
}
public void Hit(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
bool not_hit = false;
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
//射線打中物體
if (hit.collider.gameObject.GetComponent<DiskData>() != null)
{
//射中的物體要在沒有打中的飛碟串列中
for (int j = 0; j < disk_notshot.Count; j++)
{
if (hit.collider.gameObject.GetInstanceID() == disk_notshot[j].gameObject.GetInstanceID())
{
not_hit = true;
}
}
if(!not_hit)
{
return;
}
UpdateTrial();
disk_notshot.Remove(hit.collider.gameObject);
//記分員記錄分數
score_recorder.Record(hit.collider.gameObject);
//顯示爆炸粒子效果
Transform explode = hit.collider.gameObject.transform.GetChild(0);
explode.GetComponent<ParticleSystem>().Play();
//等0.1秒后執行回收飛碟
StartCoroutine(WaitingParticle(0.08f, hit, disk_factory, hit.collider.gameObject));
break;
}
}
}
//暫停幾秒后回收飛碟
IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj)
{
yield return new WaitForSeconds(wait_time);
//等待之后執行的動作
hit.collider.gameObject.transform.position = new Vector3(0, -9, 0);
disk_factory.FreeDisk(obj);
}
DiskFactory.cs是飛碟工廠,實作了生產飛碟和回收飛碟兩個方法,具體實作如下:
public class DiskFactory : MonoBehaviour
{
public GameObject disk_prefab = null; //飛碟預制體
private List<DiskData> used = new List<DiskData>(); //正在被使用的飛碟串列
private List<DiskData> free = new List<DiskData>(); //空閑的飛碟串列
public GameObject GetDisk(int round)
{
int choice = 0;
int scope1 = 1, scope2 = 4, scope3 = 7; //隨機的范圍
float start_y = -10f; //剛實體化時的飛碟的豎直位置
string tag;
disk_prefab = null;
//隨機選擇要飛出的飛碟
choice = Random.Range(0, scope3);
//將要選擇的飛碟的tag
if(choice <= scope1)
{
tag = "disk1";
}
else if(choice <= scope2 && choice > scope1)
{
tag = "disk2";
}
else
{
tag = "disk3";
}
//尋找相同tag的空閑飛碟
for(int i=0;i<free.Count;i++)
{
if(free[i].tag == tag)
{
disk_prefab = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
//如果空閑串列中沒有,則重新實體化飛碟
if(disk_prefab == null)
{
if (tag == "disk1")
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else if (tag == "disk2")
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else
{
disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);
}
//給新實體化的飛碟賦予其他屬性
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<DiskData>().color;
disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0);
disk_prefab.transform.localScale = disk_prefab.GetComponent<DiskData>().scale;
}
//添加到使用串列中
used.Add(disk_prefab.GetComponent<DiskData>());
return disk_prefab;
}
//回收飛碟
public void FreeDisk(GameObject disk)
{
for(int i = 0;i < used.Count; i++)
{
if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID())
{
used[i].gameObject.SetActive(false);
free.Add(used[i]);
used.Remove(used[i]);
break;
}
}
}
}
ScoreRecorder.cs是記分員,負責記錄分數和重置分數,具體實作如下:
public class ScoreRecorder : MonoBehaviour
{
public int score; //分數
void Start ()
{
score = 0;
}
//記錄分數
public void Record(GameObject disk)
{
int temp = disk.GetComponent<DiskData>().score;
score = temp + score;
}
//重置分數
public void Reset()
{
score = 0;
}
}
Singleton.cs在RoundController.cs中被使用,確保飛碟工廠和記分員是單例模式,具體實作如下:
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;
}
}
}
``
FlyActionManager.cs是飛碟飛行的動作管理器,利用輔助類UFOFlyAction.cs計算飛碟飛行軌跡,控制飛碟飛行,具體實作如下:
public class FlyActionManager : SSActionManager
{
public UFOFlyAction fly; //飛碟飛行的動作
public RoundController scene_controller; //當前場景的場景控制器
protected void Start()
{
scene_controller = (RoundController)SSDirector.GetInstance().CurrentScenceController;
scene_controller.action_manager = this;
}
//飛碟飛行
public void UFOFly(GameObject disk, float angle, float power, int round)
{
fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power);
fly.gravity = -0.04f - 0.01f * round;
this.RunAction(disk, fly, this);
}
}
public class UFOFlyAction : SSAction
{
public float gravity =(float) -0.05; //向下的加速度
private Vector3 start_vector; //初速度向量
private Vector3 gravity_vector = Vector3.zero; //加速度的向量,初始時為0
private float time; //已經過去的時間
private Vector3 current_angle = Vector3.zero; //當前時間的歐拉角
private UFOFlyAction() { }
public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
{
//初始化物體將要運動的初速度向量
UFOFlyAction action = CreateInstance<UFOFlyAction>();
if (direction.x < 0)
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
}
else
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
}
return action;
}
public override void Update()
{
//計算物體的向下的速度,v=at
time += Time.fixedDeltaTime;
gravity_vector.y = gravity * time;
//位移模擬
transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
transform.eulerAngles = current_angle;
//如果物體y坐標小于-3,動作就做完了
if (this.transform.position.y < -3)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
配接器版(運動與物理兼容)
專案框架如下:

為實作配接器模式,對基礎版做了如下修改:
- 增加的腳本:
ActionManagerAdapter.cs負責接收動作管理器通知,選擇采用運動學還是物理運動介面控制飛碟飛行,具體實作如下:public class ActionManagerAdapter : MonoBehaviour,IActionManager { public FlyActionManager action_manager; public PhysisFlyActionManager phy_action_manager; public void playDisk(GameObject disk, float angle, float power,int round,bool isPhy) { if(isPhy) { phy_action_manager.UFOFly(disk, angle, power,round); } else { action_manager.UFOFly(disk, angle, power,round); } } // Use this for initialization void Start () { action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager; phy_action_manager = gameObject.AddComponent<PhysisFlyActionManager>() as PhysisFlyActionManager; } }PhysisFlyActionManager.cs使用物理運動控制飛碟飛行,具體實作如下:public class PhysisFlyActionManager : SSActionManager { public PhysisUFOFlyAction fly; //飛碟飛行的動作 protected void Start() { } //飛碟飛行 public void UFOFly(GameObject disk, float angle, float power,int round) { fly = PhysisUFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power); fly.round=round; this.RunAction(disk, fly, this); } }PhysisUFOFlyAction.cs,使用運動物理計算飛碟飛行軌跡,由于重力加速度太大游戲飛碟墜落太快難度太高,需要給飛碟加一個持續的豎直向上的加速度,改變這個加速度可以使飛碟有一個隨round增大而增大的向下的加速度,具體實作如下:public class PhysisUFOFlyAction : SSAction { private Vector3 start_vector; //初速度向量 public float power; public int round; private PhysisUFOFlyAction() { } public static PhysisUFOFlyAction GetSSAction(Vector3 direction, float angle, float power) { //初始化物體將要運動的初速度向量 PhysisUFOFlyAction action = CreateInstance<PhysisUFOFlyAction>(); if (direction.x == -1) { action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power; } else { action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power; } action.power = power; return action; } public override void FixedUpdate() { //判斷是否超出范圍 if (this.transform.position.y < -3) { this.destroy = true; this.callback.SSActionEvent(this); } } public override void Update() { } public override void Start() { //使用重力加一個向上的加速度給飛碟一個隨round增大而增大的向下的加速度 gameobject.GetComponent<Rigidbody>().useGravity = true; gameobject.GetComponent<Rigidbody>().AddForce(new Vector3(0, 9.77f - 0.01f * round, 0),ForceMode.Acceleration); //給飛碟一個初速度 gameobject.GetComponent<Rigidbody>().velocity = power * 7 * start_vector; } }
- 修改的腳本:
FlyActionManager.cs去掉了對場景控制器的使用,修改后代碼如下:public class FlyActionManager : SSActionManager { public UFOFlyAction fly; //飛碟飛行的動作 protected void Start() { } //飛碟飛行 public void UFOFly(GameObject disk, float angle, float power, int round) { fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power); fly.gravity = -0.04f - 0.01f * round; this.RunAction(disk, fly, this); } }Interface.cs新增運動學與物理運動介面,具體實作如下://運動學和物理運動介面 public interface IActionManager { void playDisk(GameObject disk, float angle, float power,int round,bool isPhy); }RoundController.cs增加新的成員變數isPhy并更改對動作管理器管理飛碟飛行方法的呼叫,注意要修改action_manager的型別為IActionManager,具體實作如下:public bool isPhy = true; //是否使用物理運動 //先將action_manager的型別修改為IActionManager,在修改實體化方法的呼叫 action_manager = gameObject.AddComponent<ActionManagerAdapter>() as IActionManager; //動作管理器管理飛碟飛行方法呼叫 action_manager.playDisk(disk, angle, power,round,isPhy);SSActionManager.cs添加物理運動更新方法,具體實作如下protected void FixedUpdate() { 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.FixedUpdate(); } } foreach (int key in waitingDelete) { SSAction ac = actions[key]; actions.Remove(key); DestroyObject(ac); } waitingDelete.Clear(); }UFOFlyAction.cs添加一個空的固定更新方法:public override void FixedUpdate() { }
效果展示
游戲規則
- 游戲有無限多個round,每個round包括10次trial;
- 滑鼠點中飛碟得分,紅色飛碟得3分,綠色飛碟得2分,紫色飛碟得1分,紅飛碟體積最小,紫飛碟體積最大;
- 每次trial得分不足該次trial出現飛碟總分數50%則生命-1,round結束后生命>0則進入下一個round,否則從round 1重新開始;
- 每個trial的飛碟的色彩、大小、發射位置、速度、角度、同時出現的個數具有隨機性,總體難度隨round上升,即飛碟速度總體加快,
游戲截圖
初始界面如下,展示了游戲規則:

游戲截圖如下,界面上方展示了分數、round數、trial數、生命值:

擊中飛碟有爆炸特效:

生命值耗光,游戲結束,顯示最高記錄回合數和最高分數:

心得體會
- 加深了動作管理器的設計的理解
- 加深了對工廠模式和單實體模式的理解
- 進一步練習了使用MVC實作人機互動與游戲模型分離
- 了解了配接器模式
- 練習了剛體動力學物理引擎的使用
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/335307.html
標籤:其他
