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天空盒,最终的效果如下图所示。