Unity3d學習之路-簡單打飛碟小遊戲
簡單打飛碟小遊戲
遊戲規則與遊戲要求
規則
滑鼠點選飛碟,即可獲得分數,不同飛碟分數不一樣,飛碟的初始位置與飛行速度隨機,隨著分數增加,遊戲難度增加。初始時每個玩家都有6條生命,漏打飛碟扣除一條生命,直到生命為0遊戲結束。要求:
- 使用帶快取的工廠模式管理不同飛碟的生產與回收,該工廠必須是場景單例項的!具體實現見參考資源
Singleton
模板類 - 近可能使用前面 MVC 結構實現人機互動與遊戲模型分離
- 使用帶快取的工廠模式管理不同飛碟的生產與回收,該工廠必須是場景單例項的!具體實現見參考資源
擴充套件:
- 用自定義元件定義幾種飛碟,編輯並賦予飛碟一些屬性,做成預製
遊戲UML類圖
- 很多類都是使用之前的程式碼,修改過或者新建的類,在UML中表示出來了
遊戲實現過程
動作部分
FlyActionManager
飛碟飛行的動作管理類,當場景控制器需要飛碟飛行的時候,呼叫動作管理類的方法,讓飛碟飛行
public class FlyActionManager : SSActionManager
{
public UFOFlyAction fly; //飛碟飛行的動作
public FirstController scene_controller; //當前場景的場景控制器
protected void Start()
{
scene_controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
scene_controller.action_manager = this ;
}
//飛碟飛行
public void UFOFly(GameObject disk, float angle, float power)
{
fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power);
this.RunAction(disk, fly, this);
}
}
UFOFlyAction
給飛碟一個方向和一個力,然後飛碟模擬做有向下加速度的飛行動作,直到飛碟不在相機範圍內,就停止動作。可以設定飛碟位置y方向的值,當它小於多少的時候,不再飛行,然後等待場景控制器和飛碟工廠進行配合回收飛碟。其實可以使用
Rigidbody
元件,開啟Unity的物理模擬效果。
public class UFOFlyAction : SSAction
{
public float gravity = -5; //向下的加速度
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 == -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;
}
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座標小於-10,動作就做完了
if (this.transform.position.y < -10)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
飛碟預製與重用部分
- 用自定義元件定義幾種飛碟
使用序列化的方法,將DiskData的屬性顯示在Inspector中。需要我們新建一個指令碼,並且繼承Editor,在類的前面新增:
[CustomEditor(typeof(DiskData))]
,這裡的DiskData就是要實現自定義元件的類。在這之後新增[CanEditMultipleObjects]
,實現了多個物件可以不同的修改。如果沒有這個標籤,那麼在Inspector修改之後,擁有這個DiskData作為元件的預製體所有修改都會同步。SerializedProperty
是我們需要序列化的屬性,通過EditorGUILayout
的不同的方法,可以在Inspector中用不同方式呈現我們序列化的屬性。序列化的屬性的呈現方式需要在OnInspectorGUI
中進行編寫。
//指令碼DiskData
public class DiskData : MonoBehaviour
{
public int score = 1; //射擊此飛碟得分
public Color color = Color.white; //飛碟顏色
public Vector3 direction; //飛碟初始的位置
public Vector3 scale = new Vector3( 1 ,0.25f, 1); //飛碟大小
}
//指令碼MyDiskEditor
[CustomEditor(typeof(DiskData))]
[CanEditMultipleObjects]
public class MyDiskEditor: 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()
{
//更新serializedProperty,始終在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);
//將更改應用於serializedProperty,始終在OnInspectorGUI的末尾執行此操作
serializedObject.ApplyModifiedProperties();
}
private void ProgressBar(float value, string label)
{
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, value, label);
//中間留一個空行
EditorGUILayout.Space();
}
}
如果自定義元件只是運用在一個預製體上,也可以使用其他方法簡化程式碼,具體操作請看官方文件。我設計了三種飛碟,把DiskData指令碼掛載在每個飛碟預製體上就可以對它們進行設定了,最後實現效果如下圖。
- DiskFactory
飛碟工廠類,根據回合的不同實現隨機發送不同的飛碟。在場景控制器需要某種飛碟的時候,飛碟工廠從倉庫(
List <DiskData> free
)中獲取這種飛碟,如果倉庫中沒有,則新的例項化一個飛碟,然後新增到正在使用的飛碟列表中。當場景控制器發現飛碟被打中或者飛碟掉出攝像機視野外,將執行回收飛碟。(ps: 這裡也留了一個問題為啥List中儲存DiskData,而不直接儲存GameObject,我覺得是DiskData可以用gameObject屬性直接得到GameObject物件,而GameObject物件要用GetComponent<DiskData>()
方法去查詢DiskData元件,而GetComponent<>()比直接獲取屬性慢的多,使用DiskData頻率可能高於直接使用GameObject物件,所以優選儲存DiskData)。。。然而獲取飛碟的時候我返回了GameObject,而且在場景控制器我也是儲存的GameObject :)
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;
//根據回合,隨機選擇要飛出的飛碟
if (round == 1)
{
choice = Random.Range(0, scope1);
}
else if(round == 2)
{
choice = Random.Range(0, scope2);
}
else
{
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;
}
}
}
}
飛碟遊戲場景控制器
FirstController
遊戲有三種狀態,遊戲開始,遊戲中,遊戲結束。(最好用列舉吧,我用了三個bool變數…)。遊戲開始:設定一個定時器,定時從工廠那裡獲取飛碟,並且傳送飛碟,一次只拿一個並立即傳送。(有看過師兄的程式碼是一次就從工廠拿多個,然後慢慢傳送飛碟,如果飛碟完了又一次性拿多個)。遊戲中:根據定時器,時間到了就獲取飛碟併發送。當分數到達10分,將會多增加一種飛碟隨機發送這兩種並且縮短髮送間隔,達到25分則三種飛碟隨機發送,傳送概率不一樣。遊戲結束:顯示最高分,提供重新開始按鈕。
使用者點選螢幕傳送射線
如果打中則啟用爆炸粒子效果,一段時間後,飛碟工廠再回收。這裡使用了協程的概念,我覺得和多執行緒的意思很像,
StartCoroutine
開啟一個協程,StartCoroutine
後面的程式碼和新的協程一起執行,使用yield
暫停協程的執行,yield return
的值是代表什麼時候繼續協程的執行,這樣在yield return
後面的程式碼將延遲一點時間執行。更詳細的解釋請看官方文件。(ps: 這裡有一點迷的就是,加上協程後,使用者點選一次螢幕會執行兩次Hit函式,所以我用了判斷物體是否被打中,打中了就不要再繼續執行後面程式碼)傳送飛碟
從飛碟佇列中取出一個飛碟,然後重新設定它的位置(因為拿到的可能是使用過的飛碟)。傳送的時候檢測未射中的飛碟列表,是否已經飛出鏡頭外了,如果是使用者減一條生命。這裡直接呼叫了user_gui的方法,其實有點違背了MVC,所以如果要改進可以讓計分員記錄生命,這樣可以合併為一個場景中專門記錄數值的類。
部分程式碼如下:
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;
}
//發射射線
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;
}
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.08秒後執行回收飛碟
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);
}
//傳送飛碟
private void SendDisk()
{
float position_x = 16;
if (disk_queue.Count != 0)
{
GameObject disk = disk_queue.Dequeue();
disk_notshot.Add(disk);
disk.SetActive(true);
//設定被隱藏了或是新建的飛碟的位置
float ran_y = Random.Range(1f, 4f);
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;
//設定飛碟初始所受的力和角度
float power = Random.Range(10f, 15f);
float angle = Random.Range(15f, 28f);
action_manager.UFOFly(disk,angle,power);
}
for (int i = 0; i < disk_notshot.Count; i++)
{
GameObject temp = disk_notshot[i];
//飛碟飛出攝像機視野也沒被打中
if (temp.transform.position.y < -10 && temp.gameObject.activeSelf == true)
{
disk_factory.FreeDisk(disk_notshot[i]);
disk_notshot.Remove(disk_notshot[i]);
//玩家血量-1
user_gui.ReduceBlood();
}
}
}
一般射擊都是從點選處射出子彈,帶有碰撞器,當碰撞器與飛碟碰撞器觸碰時候,應該是開啟子彈上的粒子效果才對,然後播放完畢後子彈消失。
記分員
- ScoreRecorder
記錄分數和重置分數
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;
//Debug.Log(score);
}
//重置分數
public void Reset()
{
score = 0;
}
}
遊戲實現截圖
小結
這次遊戲使用工廠物件實現了預製體例項化後的重用,提高了遊戲效能,還加入了獲取滑鼠輸入,增加了與使用者的互動。在寫部落格的過程中發現了很多程式碼中存在的問題,也發現了許多遊戲中需要改進的地方。
完整專案請點選傳送門,Assets/Scenes/中的myScene是本次遊戲場景