unity 实现简易打飞碟游戏
一、简介
游戏共有5个回合,每个回合中会有随机产生的飞碟飞过屏幕,玩家需要做的事情就是用鼠标尽量快和多地点击飞碟。
每个飞碟对应一定的分数,目前的设置是:
【红色飞碟 3分】、【绿色飞碟 2分】、【蓝色飞碟 1分】
游戏的目的是在规定回合内得到尽可能高的分数。
二、实现效果
三、代码框架
基本延用了上一个游戏中的框架,包括动作与动作管理器、场景控制器和用户GUI,更新的部分包括:
- 动作由移动船只、移动角色变成了移动飞碟
- 将主控制器的功能分散到几个控制器上,包括分数控制器、飞碟产生器
- 增加了场景单实例的代码
四、具体实现
(一)Actions动作与动作管理器
1. SSAction 动作基类
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameObject { get; set;}
public Transform transform {get; set;}
public IActionCallback callback {get; set;}
protected SSAction() {}
// Start is called before the first frame update
public virtual void Start()
{
throw new System.NotImplementedException();
}
// Update is called once per frame
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
2. CCFlyAction 飞碟动作类
飞碟的运动有简单的两个属性:水平方向速度和垂直方向速度。
飞碟从飞碟工厂出来的时候被定位在相机视角边缘,随着运动进入相机视角,在被玩家点击或者飞出相机视角(即玩家不能再看到它时)时,飞碟和动作一起被销毁。
//飞碟从界面左右两侧飞入,离开界面时运动结束
public class CCFlyAction : SSAction
{
public float speedX;
public float speedY;
public static CCFlyAction GetSSAction(float x, float y) {
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
action.speedX = x;
action.speedY = y;
return action;
}
// Start is called before the first frame update
public override void Start()
{
}
// Update is called once per frame
public override void Update()
{
//Debug.Log("flyaction update");
if (this.transform.gameObject.activeSelf == false) {//飞碟已经被"销毁"
this.destroy = true;
this.callback.SSActionEvent(this);
return;
}
Vector3 vec3 = Camera.main.WorldToScreenPoint (this.transform.position);
if (vec3.x < -100 || vec3.x > Camera.main.pixelWidth + 100 || vec3.y < -100 || vec3.y > Camera.main.pixelHeight + 100) {
this.destroy = true;
this.callback.SSActionEvent(this);
return;
}
transform.position += new Vector3(speedX, speedY, 0) * Time.deltaTime * 2;
}
}
3. IActionCallback 事件回调接口
public enum SSActionEventType:int {Started, Completed}
public interface IActionCallback
{
//回调函数
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
4. SSActionManager 动作管理类基类
新增了一个函数RemainActionCount(),用于判断每回合中剩余的动作数量(即飞碟数量),只有这个值为0才进入下一回合。
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>();
// Start is called before the first frame update
protected void Start()
{
}
// Update is called once per frame
protected void Update()
{
//Debug.Log("ssactionmanager update");
foreach (SSAction ac in waitingAdd) {
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();
//Debug.Log(actions.Count);
foreach(KeyValuePair<int, SSAction> kv in actions) {
SSAction ac = kv.Value;
if (ac.destroy) {
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable) {
//Debug.Log("ssactionmanager update");
ac.Update();
}
}
foreach(int key in waitingDelete) {
SSAction ac = actions[key];
actions.Remove(key);
Destroy(ac);
}
waitingDelete.Clear();
}
public void RunAction(GameObject gameObject, SSAction action, IActionCallback manager) {
//Debug.Log("run action");
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
public int RemainActionCount() {
return actions.Count;
}
}
5. CCActionManager 动作管理类
动作结束时会调用动作管理者实现的回调函数,即IActionCallback接口中的SSActionEventType,动作管理者将动作绑定的游戏对象(飞碟)销毁。
public class CCActionManager : SSActionManager, IActionCallback
{
public RoundController sceneController;
public CCFlyAction action;
// Start is called before the first frame update
protected new void Start()
{
sceneController = (RoundController)SSDirector.getInstance().currentSceneController;
sceneController.actionManager = this;
}
// Update is called once per frame
// protected new void Update()
// {
// }
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null) {
factory.FreeDisk(source.transform.gameObject);
}
public void MoveDisk(GameObject disk) {
action = CCFlyAction.GetSSAction(disk.GetComponent<DiskAttributes>().speedX, disk.GetComponent<DiskAttributes>().speedY);
RunAction(disk, action, this);
}
}
(二)Controllers 控制器
1. DiskFactory 飞碟生成器
用于生产飞碟。
GetDisk(int round)被主控制器调用,round(回合数)会影响所生产的飞碟的速度、大小等属性。
有两个列表used和free,存放的是飞碟属性(包括分数、速度),可以循环使用,提高飞碟的产生效率。
飞碟初始位置随机,可能为屏幕的四个角落之一。
根据飞碟的分数和回合数设置飞碟的大小和速度。
public class MyException : System.Exception
{
public MyException() { }
public MyException(string message) : base(message) { }
}
public class DiskAttributes : MonoBehaviour
{
//public GameObject gameobj;
public int score;
public float speedX;
public float speedY;
}
public class DiskFactory : MonoBehaviour
{
List<GameObject> used;
List<GameObject> free;
System.Random rand;
// Start is called before the first frame update
void Start()
{
used = new List<GameObject>();
free = new List<GameObject>();
rand = new System.Random();
//Disk disk = GetDisk(1);
}
public GameObject GetDisk(int round) {
GameObject disk;
if (free.Count != 0) {
disk = free[0];
free.Remove(disk);
}
else {
disk = GameObject.Instantiate(Resources.Load("Prefabs/disk", typeof(GameObject))) as GameObject;
disk.AddComponent<DiskAttributes>();
}
//根据不同round设置diskAttributes的值
//随意的旋转角度
disk.transform.localEulerAngles = new Vector3(-rand.Next(20,40),0,0);
DiskAttributes attri = disk.GetComponent<DiskAttributes>();
attri.score = rand.Next(1,4);
//由分数来决定速度、颜色、大小
attri.speedX = (rand.Next(1,5) + attri.score + round) * 0.2f;
attri.speedY = (rand.Next(1,5) + attri.score + round) * 0.2f;
if (attri.score == 3) {
disk.GetComponent<Renderer>().material.color = Color.red;
disk.transform.localScale += new Vector3(-0.5f,0,-0.5f);
}
else if (attri.score == 2) {
disk.GetComponent<Renderer>().material.color = Color.green;
disk.transform.localScale += new Vector3(-0.2f,0,-0.2f);
}
else if (attri.score == 1) {
disk.GetComponent<Renderer>().material.color = Color.blue;
}
//飞碟可从四个方向飞入(左上、左下、右上、右下)
int direction = rand.Next(1,5);
if (direction == 1) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 1.5f, 8)));
attri.speedY *= -1;
}
else if (direction == 2) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 0f, 8)));
}
else if (direction == 3) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 1.5f, 8)));
attri.speedX *= -1;
attri.speedY *= -1;
}
else if (direction == 4) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 0f, 8)));
attri.speedX *= -1;
}
used.Add(disk);
disk.SetActive(true);
Debug.Log("generate disk");
return disk;
}
public void FreeDisk(GameObject disk) {
disk.SetActive(false);
//将位置和大小恢复到预制,这点很重要!
disk.transform.position = new Vector3(0, 0,0);
disk.transform.localScale = new Vector3(2f,0.1f,2f);
if (!used.Contains(disk)) {
throw new MyException("Try to remove a item from a list which doesn't contain it.");
}
Debug.Log("free disk");
used.Remove(disk);
free.Add(disk);
}
}
2. Singleton 单实例代码
用于单实例化飞碟工厂
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;
}
}
}
可以这样使用:
DiskFactory factory = Singleton<DiskFactory>.Instance;
3. ISceneController 场景控制器接口
其中包含的方法必须被主控制器实现。
public interface ISceneController
{
void LoadSource();
}
4. SSDirector 导演类
public class SSDirector : System.Object
{
private static SSDirector _instance;
public ISceneController currentSceneController {get; set;}
public static SSDirector getInstance() {
if (_instance == null) {
_instance = new SSDirector();
}
return _instance;
}
}
5. ScoreController 计分器
初始化时将自身设置为主控制器的分数控制器。
public class ScoreController : MonoBehaviour
{
int score;
public RoundController roundController;
public UserGUI userGUI;
// Start is called before the first frame update
void Start()
{
roundController = (RoundController)SSDirector.getInstance().currentSceneController;
roundController.scoreController = this;
userGUI = this.gameObject.GetComponent<UserGUI>();
}
public void Record(GameObject disk) {
score += disk.GetComponent<DiskAttributes>().score;
userGUI.score = score;
}
}
6. RoundController 主控制器
连接用户与游戏,分别需要实现场景控制器的接口和用户操作的接口。
Update的主要工作:在每个回合中从工厂获取飞碟,为飞碟绑定动作,令其开始运动。
public class RoundController : MonoBehaviour, ISceneController, IUserAction
{
int round = 0;
int max_round = 5;
float timer = 0.5f;
GameObject disk;
DiskFactory factory ;
public CCActionManager actionManager;
public ScoreController scoreController;
public UserGUI userGUI;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (userGUI.mode == 0) return;
GetHit();
gameOver();
if (round > max_round) {
return;
}
timer -= Time.deltaTime;
if (timer <= 0 && actionManager.RemainActionCount() == 0) {
//从工厂中得到10个飞碟,为其加上动作
for (int i = 0; i < 10; ++i) {
disk = factory.GetDisk(round);
actionManager.MoveDisk(disk);
//Thread.Sleep(100);
}
round += 1;
if (round <= max_round) {
userGUI.round = round;
}
timer = 4.0f;
}
}
void Awake() {
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
director.currentSceneController.LoadSource();
gameObject.AddComponent<UserGUI>();
gameObject.AddComponent<CCActionManager>();
gameObject.AddComponent<ScoreController>();
gameObject.AddComponent<DiskFactory>();
factory = Singleton<DiskFactory>.Instance;
userGUI = gameObject.GetComponent<UserGUI>();
}
public void LoadSource()
{
}
public void gameOver()
{
if (round > max_round && actionManager.RemainActionCount() == 0)
userGUI.gameMessage = "Game Over!";
}
public void GetHit() {
if (Input.GetButtonDown("Fire1")) {
Camera ca = Camera.main;
Ray ray = ca.ScreenPointToRay(Input.mousePosition);
//Return the ray's hit
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
scoreController.Record(hit.transform.gameObject);
hit.transform.gameObject.SetActive(false);
}
}
}
}
(三)Viewers 用户接口与用户GUI
1. IUserAction 用户接口
public interface IUserAction {
void gameOver();
void GetHit();
}
2. UserGUI 用户界面
public class UserGUI : MonoBehaviour
{
public int mode;
public int score;
public int round;
public string gameMessage;
private IUserAction action;
public GUIStyle bigStyle, blackStyle, smallStyle;//自定义字体格式
public Font pixelFont;
private int menu_width = Screen.width / 5, menu_height = Screen.width / 10;//主菜单每一个按键的宽度和高度
// Start is called before the first frame update
void Start()
{
mode = 0;
gameMessage = "";
action = SSDirector.getInstance().currentSceneController as IUserAction;
//pixelStyle
//pixelFont = Font.Instantiate(Resources.Load("Fonts/ThaleahFat", typeof(Font))) as Font;
//if (pixelFont == null) Debug.Log("null");
//pixelFont.fontSize = 50;
//pixelFont = Arial;
//大字体初始化
bigStyle = new GUIStyle();
bigStyle.normal.textColor = Color.white;
bigStyle.normal.background = null;
bigStyle.fontSize = 50;
bigStyle.alignment=TextAnchor.MiddleCenter;
//black
blackStyle = new GUIStyle();
blackStyle.normal.textColor = Color.black;
blackStyle.normal.background = null;
blackStyle.fontSize = 50;
blackStyle.alignment=TextAnchor.MiddleCenter;
//小字体初始化
smallStyle = new GUIStyle();
smallStyle.normal.textColor = Color.white;
smallStyle.normal.background = null;
smallStyle.fontSize = 20;
smallStyle.alignment=TextAnchor.MiddleCenter;
}
// Update is called once per frame
void Update()
{
}
void OnGUI() {
//GUI.skin.button.font = pixelFont;
GUI.skin.button.fontSize = 35;
switch(mode) {
case 0:
mainMenu();
break;
case 1:
GameStart();
break;
}
}
void mainMenu() {
GUI.Label(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 0.1f, menu_width, menu_height), "Hit UFO", bigStyle);
bool button = GUI.Button(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 3 / 7, menu_width, menu_height), "Start");
if (button) {
mode = 1;
}
}
void GameStart() {
GUI.Label(new Rect(300, 60, 50, 200), gameMessage, bigStyle);
GUI.Label(new Rect(0,0,100,50), "Score: " + score, smallStyle);
GUI.Label(new Rect(560,0,100,50), "Round: " + round, smallStyle);
}
}
五、项目地址
六、可改进的地方
- 寻找合适的飞碟或飞盘贴图,增加自旋效果
- 给飞碟加上更复杂的轨迹,比如上下左右起伏
- 增加击中飞碟时的画面效果,比如飞碟破碎或爆炸