这一次我们来做的任务是3D版鼠标点击鼠标的游戏。我们先来看一下游戏需求。
案例研究:“鼠标打飞碟”游戏设计 游戏需求:
1. 分多个 round , 飞碟数量每个 round 都是 n 个
2. 每个 round 的飞碟的色彩,大小;发射位置,速度, 角度,每次发射飞碟数量不一样。
3. 鼠标击中得分,得分按色彩、大小、速度不同计算,规则自己定
先考虑一下游戏的对象要有飞碟,而且可能会需要很多个飞碟,再加上分多个round,这样游戏的总飞碟数需要很多,首先不断地去new GameObject需要消耗比较多内存,还有可能让游戏卡顿的情况。所以这时候我们需要建立一个游戏工厂由它负责去回收,产生飞碟。游戏工厂的好处在于要减少对象的创建与销毁,而且屏蔽创建与销毁的逻辑,使程序易于扩展,并且分离出场记的任务,使游戏架构更加清晰明了。
要实现回收和利用我们可以使用两个链表used,free分别表示使用中的飞碟和空闲的飞碟。然后在Diskfactory里面实现两个函数getDisk,freeDisk。我们看下伪代码先。
getDisk(ruler) //伪代码
IF (free list has disk)
THEN
a_disk = remove one from list
ELSE
a_disk = clone from Prefabs
ENDIF
Set DiskData of a_disk with the ruler
Add a_disk to used list
Return a_disk
FreeDisk(disk) //伪代码
Find disk in used list
IF (not found)
THEN THROW exception
Move disk from used to free list
接下来就可以来具体用C#语言实现
public GameObject getDisk(int round)
{
if (sceneControler.num == 31 && sceneControler.Score >= round * 20)
//每轮总共发射30个,如果得分达到一定要求进入下一轮,否则GameOver
{
sceneControler.round++;
sceneControler.num = 0;
}
else if(sceneControler.num == 31 && sceneControler.Score < round * 20)
{
sceneControler.game = 2;//游戏结束
}
GameObject newDisk;
if (free.Count == 0)
{
newDisk = GameObject.Instantiate(Resources.Load("prefabs/Disk")) as GameObject;
Debug.Log("111");
}
else
{
newDisk = free[0];
free.Remove(free[0]);
}
switch (round)
//根据轮数制定飞碟的颜色和大小
{
case 1:
newDisk.transform.localScale = new Vector3(DiskData.round1.size, DiskData.round1.size, DiskData.round1.size);
newDisk.GetComponent<Renderer>().material.color = DiskData.round1.color;
break;
case 2:
newDisk.transform.localScale = new Vector3(DiskData.round2.size, DiskData.round2.size, DiskData.round2.size);
newDisk.GetComponent<Renderer>().material.color = DiskData.round2.color;
break;
case 3:
newDisk.transform.localScale = new Vector3(DiskData.round3.size, DiskData.round3.size, DiskData.round3.size);
newDisk.GetComponent<Renderer>().material.color = DiskData.round3.color;
break;
}
used.Add(newDisk);
return newDisk;
}
public void freeDisk(GameObject disk1)
{
for (int i = 0; i < used.Count; i++)
{
if (used[i] == disk1)
{
used.Remove(disk1);
disk1.SetActive(true);//被鼠标击中的disk设置为false,所以这里全部激活一遍
free.Add(disk1);
}
}
return;
}
接着实现工厂定义,这里请原谅我的年幼无知,将DiskFactory定义成System.Object,这样的弊端很大,所有的Scene都能访问到它,下一篇博客我会就此改进的。
public class DiskFactory : System.Object
{
private static DiskFactory _instance;
public SceneController sceneControler { get; set; }
public List<GameObject> used;
public List<GameObject> free;
// Use this for initialization
public static DiskFactory getInstance()
{
if (_instance == null)
{
_instance = new DiskFactory();
_instance.used = new List<GameObject>();
_instance.free = new List<GameObject>();
}
return _instance;
}
接下来我们考虑下飞碟的动作Emit,难点在于如何实现曲线运动。我们可以通过化直为曲的方式实现,每一帧计算弧线夹角去平移,这样看起来就是曲线了,先定义动作基类。
public enum SSActionEventType : int { Started, Competeted }
public interface ISSActionCallback
{
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null);
}
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameobject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }
protected SSAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
然后定义曲线运动
public override void Update()
{
Vector3 targetPos = target.transform.position;
//让始终它朝着目标
gameobject.transform.LookAt(targetPos);
//计算弧线中的夹角
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.transform.position);
gameobject.transform.Translate(Vector3.forward * Mathf.Min(speed * Time.deltaTime, currentDist));
if (this.transform.position == target.transform.position)
{
DiskFactory.getInstance().freeDisk(gameobject);//到达终点就free
Destroy(target);
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
接下来完善Emit的定义
public class Emit2 : SSAction
{
public SceneController sceneControler = (SceneController)SSDirector.getInstance().currentScenceController;
public GameObject target; //要到达的目标
public float speed; //速度
private float distanceToTarget; //两者之间的距离
float startX;
float targetX;
float targetY;
public override void Start()
{
speed = 5 + sceneControler.round * 5;//使速度随着轮数变化
startX = 6 - Random.value * 12;//使发射位置随机在(-6,6)
if (Random.value > 0.5)
{
targetX = 32;
}
else
{
targetX = -32;
}
targetY = (Random.value * 25);//随机在(0,25)
this.transform.position = new Vector3(startX, 0, 0);
target = new GameObject();//创建终点
target.transform.position = new Vector3(targetX, targetY, 30);
//计算两者之间的距离
distanceToTarget = Vector3.Distance(this.transform.position, target.transform.position);
}
public static Emit2 GetSSAction()
{
Emit2 action = ScriptableObject.CreateInstance<Emit2>();
return action;
}
不要以为那些位置值是凭空出来的,都是用一个具体cube,不断微调它的位置让它恰好在摄像机屏幕最下方的完整的一行,终点也是如此,微调到Cube恰好出屏幕的一个位置,便于最快速回收,太远会飞很久然后需要跟多实例飞碟,所以说往往细节决定成败。FIGHTING!!!
然后定义下动作管理器的基类和派生类。
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>();
// Use this for initialization
void Start()
{
}
// 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.destroy)
{
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable)
{
ac.Update();
}
}
foreach (int key in waitingDelete)
{
SSAction ac = actions[key]; actions.Remove(key); DestroyObject(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();
}
}
public class CCActionManager : SSActionManager, ISSActionCallback
{
public SceneController sceneController;
public DiskFactory diskFactory;
public Emit2 EmitDisk;
int count = 0;
// Use this for initialization
protected void Start()
{
sceneController = (SceneController)SSDirector.getInstance().currentScenceController;
diskFactory = DiskFactory.getInstance();
sceneController.actionManager = this;
}
// Update is called once per frame
protected new void Update()
{
if (sceneController.round <= 3 && sceneController.game == 1)
{
count++;
if (count == 60)//实现60帧发射一个飞碟
{
EmitDisk = Emit2.GetSSAction();
this.RunAction(diskFactory.getDisk(sceneController.round), EmitDisk, this);
sceneController.num++;//记录发射数量
print(sceneController.num);
count = 0;//满60帧置零实现循环
}
base.Update();
}
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
//
}
}
然后实现用户接口实现功能显示细节,开始游戏,重新开始游戏。
public interface IUserAction
{
void StartGame();//开始游戏
void ShowDetail();//显示细节,介绍游戏
void ReStart();//重新开始游戏
}
public class UserGUI : MonoBehaviour
{
private IUserAction action;
// Use this for initialization
void Start()
{
action = SSDirector.getInstance().currentScenceController as IUserAction;
}
void OnGUI()
{
GUIStyle fontstyle1 = new GUIStyle();
fontstyle1.fontSize = 50;
fontstyle1.normal.textColor = new Color(255, 255, 255);
if (GUI.RepeatButton(new Rect(0, 0, 120, 40), "Shooting Disk"))
{
action.ShowDetail();
}
if (GUI.Button(new Rect(0, 60, 120, 40), "STARTGAME"))
{
action.StartGame();
}
if (GUI.Button(new Rect(0, 120, 120, 40), "RESTART"))
{
action.ReStart();
}
}
// Update is called once per frame
void Update()
{
//
}
}
最后实现下导演和场记就大功告成。
public interface ISceneController
{
void LoadResources();
//void Pause ();
//void Resume ();
}
public class SSDirector : System.Object
{
private static SSDirector _instance;
public ISceneController currentScenceController { get; set; }
public bool running { get; set; }
public static SSDirector getInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
public int getFPS()
{
return Application.targetFrameRate;
}
public void setFPS(int fps)
{
Application.targetFrameRate = fps;
}
}
做之前我们想一想,点击完飞碟直接设置active为false,就看不见了是不是有点不过瘾,少了点爆炸效果呢,如何做到呢,稍稍利用下PaticleSystem就行了。具体设置如下,我就不具体介绍了自己查查资料就知道了
还有如果只看到点击,没看到分数和轮数是不是有点遗憾,试用下Unity3D的GUI建立Text出来就行啦
具体属性可以在检视面板设置,自己去尝试尝试就知道啦
写出Updae如下
// Update is called once per frame
void Update()
{
ScoreText.text = "Score:" + Score.ToString();
RoundText.text = "Round:" + round.ToString();
if (Input.GetMouseButtonDown(0) && game == 1)
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if (hit.transform.tag == "Disk")
{
explosion.transform.position = hit.collider.gameObject.transform.position;
explosion.GetComponent<Renderer>().material = hit.collider.gameObject.GetComponent<Renderer>().material;
explosion.GetComponent<ParticleSystem>().Play();
hit.collider.gameObject.SetActive(false);
print("Hit!!!");
Score += round;
}
}
}
if(game == 2)
{
GameOver();
}
}
然后想着如果点击游戏开始直接开始了是不是很糟糕,玩家都没时间准备的,那就来个倒计时吧。如何实现呢?通过协程实现就行啦,什么是协程呢?请看博客
协程.
public IEnumerator waitForOneSecond()
{
while (CoolTimes >= 0 && game == 3)
{
GameText.text = CoolTimes.ToString();
print("还剩" + CoolTimes);
yield return new WaitForSeconds(1);
CoolTimes--;
}
GameText.text = "";
game = 1;//游戏开始
}
我们只要在StartGame中调用即可
public void StartGame()
{
num = 0;
if (game == 0)
{
game = 3;//进入倒计时状态
StartCoroutine(waitForOneSecond());
}
}
接下来实现下其他User接口函数和其他定义即可
public void ReStart()
{
SceneManager.LoadScene("task1");
game = 0;
}
public void ShowDetail()
{
GUI.Label(new Rect(220, 50, 350, 250), "Use your mouse click disk, you will get 1 point for green Disk,2 for yellow Disk,3 for red Disk,you should get 20 points to pass round1,40 to pass round2,60 to pass round3.There are three round.Good Luck!!!");
}
public class SceneController : MonoBehaviour, ISceneController, IUserAction
{
public SSActionManager actionManager { get; set; }
public int round = 0;//轮数
public float Score = 0;//分数
public Text ScoreText;//分数文本
public Text RoundText;//轮数文本
public Text GameText;//倒计时文本
public Text FinalText;//结束文本
public int game = 0;//记录游戏进行情况
public int num = 0;//每轮的飞碟数量
GameObject disk;
GameObject explosion;
public int CoolTimes = 3; //准备时间
// Use this for initialization
void Awake()
//创建导演实例并载入资源
{
SSDirector director = SSDirector.getInstance();
DiskFactory DF = DiskFactory.getInstance();
DF.sceneControler = this;
director.setFPS(60);
director.currentScenceController = this;
director.currentScenceController.LoadResources();
}
void Start()
{
round = 1;
}
public void LoadResources()
{
explosion = Instantiate(Resources.Load("prefabs/Explosion"), new Vector3(-40, 0, 0), Quaternion.identity) as GameObject;
Instantiate(Resources.Load("prefabs/Light"));
}
public void GameOver()
{
FinalText.text = "Game Over!!!";
}
最后上个类图和成果图帮大家理理关系
(告诫大家一个事,是我的亲身经历,本来我周四晚上已经做完了,但是为了选飞碟模型上网去下载了模型,想套用但是发现它不能变颜色,所以我删掉它但是不小心把整个文件夹都删了,我还以为在回收站能找回,ctrl+z能撤回,我想太多了,百度一下,答案都是删除文件夹就没有,我能怎么办,我也很绝望,打了一天的代码重打一次。
)