github傳送門:https://github.com/dongzizhu/unity3DLearning/tree/master/hw4/Disk
視頻傳送門:https://space.bilibili.com/472759319
打飛碟小遊戲
這次的代碼架構同樣採用了MVC模式,與之前的牧師與魔鬼基本相同,這裏就不重複敘述了,感興趣的可以看上上篇博文。
這裏主要還是介紹一下firstController的變化以及新應用的工廠模式和真正負責飛碟移動的Emit類。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using MyGame;
using UnityEngine.SceneManagement;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
public ActionManager MyActionManager { get; set; }
public DiskFactory factory { get; set; }
public RecordController scoreRecorder;
public UserGUI user;
void Awake() {
Director diretor = Director.getInstance();
diretor.sceneCtrl = this;
}
// Use this for initialization
void Start() {
Begin();
}
// Update is called once per frame
void Update () {
}
public void Begin() {
MyActionManager = gameObject.AddComponent<ActionManager>() as ActionManager;
scoreRecorder = gameObject.AddComponent<RecordController>();
user = gameObject.AddComponent<UserGUI>();
user.Begin();
}
public void Hit(DiskController diskCtrl) {
// 0=playing 1=lose 2=win 3=cooling
if (user.game == 0) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
//hit.collider.gameObject.SetActive(false);
Debug.Log("Hit");
factory.freeDisk(hit.collider.gameObject);
hit.collider.gameObject.GetComponent<DiskController>().hit = true;
scoreRecorder.add(hit.collider.gameObject.GetComponent<DiskController>());
}
}
}
public void PlayDisk() {
MyActionManager.playDisk(user.round);
}
public void Restart() {
SceneManager.LoadScene("scene");
}
public int Check() {
return 0;
}
}
FirstController同樣是負責着所有其他的controller和userGUI,這都和之前相同;新加入的DiskFactory我們一會兒再介紹。這裏主要講一下Hit函數。所謂ScreenPointToRay就是從Camera出發連接到鼠標點擊位置的一條射線,然後如果射線經過了我們目標的GameObject,就算是擊中了。當一個飛碟被擊中時,我們首先將這個Object的Active設爲False,從而將擊中的消息傳回給Action;然後FreeDisk是將這個實例從放到free列表中等待下一次調用(其實在FreeDisk中我們已經有了設置Active的操作,這裏將其註釋在這裏是爲了提醒我們它的重要性)。不知道free列表是什麼東西沒關係,我們繼續看DiskFactory的代碼。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MyGame;
public class DiskFactory : MonoBehaviour {
private static DiskFactory _instance;
public FirstController sceneControler { get; set; }
GameObject diskPrefab;
public DiskController diskData;
public List<GameObject> used;
public List<GameObject> free;
// Use this for initialization
public static DiskFactory getInstance() {
return _instance;
}
private void Awake() {
if (_instance == null) {
_instance = Singleton<DiskFactory>.Instance;
_instance.used = new List<GameObject>();
_instance.free = new List<GameObject>();
diskPrefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk"), new Vector3(40, 0, 0), Quaternion.identity);
}
}
public void Start() {
sceneControler = (FirstController)Director.getInstance().sceneCtrl;
sceneControler.factory = _instance;
}
public GameObject getDisk(int round) { // 0=playing 1=lose 2=win 3=cooling
if (sceneControler.scoreRecorder.Score >= round * 4) {
if (sceneControler.user.round < 3) {
sceneControler.user.round++;
sceneControler.user.num = 0;
sceneControler.scoreRecorder.Score = 0;
}
else {
sceneControler.user.game = 2; // 贏了
return null;
}
}
else {
if (sceneControler.user.num >= 10) {
sceneControler.user.game = 1; // 輸了
return null;
}
}
GameObject newDisk;
RoundController diskOfCurrentRound = new RoundController(sceneControler.user.round);
if (free.Count == 0) {// if no free disk, then create a new disk
newDisk = GameObject.Instantiate(diskPrefab) as GameObject;
newDisk.AddComponent<ClickGUI>();
diskData = newDisk.AddComponent<DiskController>();
}
else {// else let the first free disk be the newDisk
newDisk = free[0];
free.Remove(free[0]);
newDisk.SetActive(true);
}
diskData = newDisk.GetComponent<DiskController>();
diskData.color = diskOfCurrentRound.color;
//Debug.Log(diskData);
newDisk.transform.localScale = diskOfCurrentRound.scale * diskPrefab.transform.localScale;
newDisk.GetComponent<Renderer>().material.color = diskData.color;
used.Add(newDisk);
return newDisk;
}
public void freeDisk(GameObject disk1) {
used.Remove(disk1);
disk1.SetActive(false);
free.Add(disk1);
return;
}
public void Restart() {
used.Clear();
free.Clear();
}
}
這就是所謂的工廠模式了。當遊戲對象的創建與銷燬成本較高,且遊戲涉及大量遊戲對象的創建與銷燬時,必須考慮減少銷燬次數,比如這次的打飛碟遊戲,或者像其他類型的射擊遊戲,其中子彈或者中彈對象的創建與銷燬是很頻繁的。工廠模式將已經創建好正在使用的實例存在一個used列表中,然後當使用完成(被擊中)就將其放在free列表中,等待下一次調用;當我們需要一個新的實例的時候,首先檢查free列表,當其中沒有限制的實例時我們才創建一個新的。getDisk和freeDisk就實現了上面所敘述的邏輯,是核心的代碼。
最後是Emit類。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MyGame;
public class Emit : SSAction
{
public FirstController sceneControler = (FirstController)Director.getInstance().sceneCtrl;
public Vector3 target;
public float speed;
private float distanceToTarget;
float startX;
float targetX;
float targetY;
public override void Start() {
speed = sceneControler.user.round * 5;
GameObject.GetComponent<DiskController>().speed = speed;
startX = 6 - Random.value * 12;
if (Random.value > 0.5) {
targetX = 36 - Random.value * 36;
targetY = 25 - Random.value * 25;
}
else {
targetX = -36 + Random.value * 36;
targetY = -25 + Random.value * 25;
}
this.Transform.position = new Vector3(startX, 0, 0);
target = new Vector3(targetX, targetY, 30);
//Debug.Log(target);
distanceToTarget = Vector3.Distance(this.Transform.position, target);
}
public static Emit GetSSAction() {
Emit action = ScriptableObject.CreateInstance<Emit>();
return action;
}
public override void Update() {
Vector3 targetPos = target;
if(!GameObject.activeSelf){
this.destroy = true;
return;
}
//facing the target
GameObject.transform.LookAt(targetPos);
//calculate the starting angel
float angle = Mathf.Min(1, Vector3.Distance(GameObject.transform.position, targetPos) / distanceToTarget) * 45;
GameObject.transform.rotation = GameObject.transform.rotation * Quaternion.Euler(Mathf.Clamp(-angle, -42, 42), 0, 0);
float currentDist = Vector3.Distance(GameObject.transform.position, target);
//Debug.Log("****************************");
//Debug.Log(startX);
//Debug.Log(target);
//Debug.Log("****************************");
GameObject.transform.Translate(Vector3.forward * Mathf.Min(speed * Time.deltaTime, currentDist));
if (this.Transform.position == target) {
sceneControler.scoreRecorder.miss();
Debug.Log("here in miss!!");
GameObject.SetActive(false);
GameObject.transform.position = new Vector3(startX, 0, 0);
sceneControler.factory.freeDisk(GameObject);
this.destroy = true;
this.Callback.ActionDone(this);
}
}
}
我們在Start函數中保證了飛碟出現的位置和目標方向的隨機性。然後在Update函數中首先計算移動的角度,然後根據速度給出當前的位移,然後進行一次判斷,如果當前位置已經是終點了,那麼我們首先setActive告訴外層的actionManager之前的運動可以取消了,然後將當前的實例free掉。在Update函數開始返回前也需要設置一下destroy是爲了在hit後也可以告訴actionManager取消當前運動,與後面那個並不是重複操作。
最後我們來看一眼actionControl的核心ActionMananger,其他的就不全部貼上來了。
public class ActionManager : SSActionManager {
public FirstController sceneController;
public DiskFactory diskFactory;
public RecordController scoreRecorder;
public Emit EmitDisk;
public GameObject Disk;
int count = 0;
protected void Start() {
sceneController = (FirstController)Director.getInstance().sceneCtrl;
diskFactory = sceneController.factory;
scoreRecorder = sceneController.scoreRecorder;
sceneController.MyActionManager = this;
}
protected new void Update() {
if (sceneController.user.round <= 3 && sceneController.user.game == 0) {
count++;
if (count == 60 * sceneController.user.round) {
playDisk(sceneController.user.round);
sceneController.user.num++;
count = 0;
}
base.Update();
}
}
public void playDisk(int round) {
EmitDisk = Emit.GetSSAction();
Disk = diskFactory.getDisk(round);
this.AddAction(Disk, EmitDisk, this);
Disk.GetComponent<DiskController>().action = EmitDisk;
}
}
Update實現了每60幀*round後發出一個飛碟。之所以這樣設計是因爲最後一輪的飛碟速度太快,這樣能夠適當降低遊戲難度。playDisk函數就是從工廠中獲取一個飛碟,然後和下一個應該出現的飛碟的移動方向和特徵一起傳給AcitionManager。
另外爲了有空戰的感覺,我還加入了在AssetStore下載的StarField天空盒,最終的效果如下圖所示。