Unity3D小游戏——打飞碟
首先对该游戏框架进行分析:打飞碟这个游戏中只有“飞碟”这一类游戏对象,因此首先需要UFO.cs类用来保存飞碟的gameObject和飞碟的大小、速度、移动方向、颜色等单个飞碟独有的属性。然后,分析该飞碟的行为:它可以被点击、它在视线范围内时遵循抛物线的飞行轨迹、它飞行到视线范围外时自动消失。对于第一个行为和第三个行为,我分别定义了Click.cs和MoveOut.cs组件执行该动作。而对于抛物线的飞行轨迹,我通过调用Unity中既成的Rigidbody模块实现。我们还需要一个飞碟控制器对游戏中所有的飞碟进行管理。根据要求,该控制器被实现为工厂+单例模式,在代码中我定义为UFOFatory.cs文件。此外,根据上次作业的经验,我们还需要一个场景控制器SceneController.cs、一个导演类Director.cs和管理GUI类UserGUI.cs。以上七个文件构成了本次作业的全部代码。
文件的结构如下。其中Resource/Prefabs文件夹中只有一个椭圆形预制体代表飞碟。
导演需要作为游戏的系统对象,并且实现为单例模式。单例的作用是保证其余的所有控制器都在同一个导演的管理之下。导演记录了当前活跃的场景管理器(虽然本游戏中只有一种场景管理器)
public class Director : System.Object { static Director _instance; public SceneController CurrentSceneController {get; set;} public static Director GetInstance() { if (_instance == null) { _instance = new Director(); } return _instance; } }
场景管理器对该场景内的全局变量和动作做管理,它控制全局游戏的开始和结束,同时管理一轮游戏开始时下一轮游戏会自动启动。另外当一个飞碟被点击、飞出视线外时,会使游戏的全局变量(得分、血量、轮数)发生变化,场景管理器也要对这个功能进行维护。另外在实现时,我创建了“飞碟工厂”这个游戏对象,把UFOFatory.cs挂到该游戏对象上,保证了飞碟工厂的单实例。
public class SceneController : MonoBehaviour { public int round; public int score; public int blood; public bool isStart; public bool isFailed; public int currentDifficulty; GameObject fatory; public void Awake(){ Director director = Director.GetInstance(); director.CurrentSceneController = this; director.CurrentSceneController.Initialize(); fatory = new GameObject("UFO fatory"); fatory.AddComponent<UFOFatory>(); fatory.GetComponent<UFOFatory>().SetDepend(this); } //对场景参数进行初始化 public void Initialize(){ round = 1; score = 0; blood = 10; currentDifficulty = 0; isStart = false; isFailed = false; } //点击飞碟时触发加分 public void AddScore(){ score += 1; } //试去一个飞碟时触发减分 public void SubBlood(){ if(blood > 0){ blood -= 1; } if(blood == 0){ isFailed = true; fatory.GetComponent<UFOFatory>().InitializeUFO(); isStart = false; } } //开始一次新游戏 public void StartNewGame(){ Initialize(); isStart = true; StartRound(); } //开始一轮抛掷飞碟(10个) public void StartRound(){ fatory.GetComponent<UFOFatory>().SetDifficulty(currentDifficulty); fatory.GetComponent<UFOFatory>().InitializeUFO(); fatory.GetComponent<UFOFatory>().StartRound(); } //当一轮游戏结束,通知开始下一轮游戏 public void RoundDone(){ round++; currentDifficulty++; StartRound(); } }
飞碟工厂是本次作业中最重要的部分。飞碟工厂控制飞碟的行为,在一轮抛飞碟的动作中,负责管理所有飞碟的初始化、逐个抛出和回收,以及通过难度决定飞碟的大小和抛出飞碟的间隔。在游戏过程中,飞碟对象被一直缓存在飞碟控制器中。老师建议的做法和以前师兄们留下的博客不同,我在这里使用了协程解决了“定时间隔抛出飞碟”这一动作。使用协程后代码更加简单容易维护。唯一要注意的是,当一轮游戏结束或者生命值耗尽时,要主动中断协程。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; public class UFOFatory : MonoBehaviour { public int difficulty; UFO[] UFOs = new UFO[trial]; public SceneController myController; public static int trial = 10; Coroutine mCoroutine = null; // 当前的协程 public static Color[] colors = new Color[5]{Color.red, Color.black, Color.blue, Color.green, Color.yellow}; public static float[] scales = new float[5]{0.6f, 0.8f, 1.0f, 1.2f, 1.4f}; public static float[] y_position = new float[5]{1f,2f,3f,4f,5f}; public static float[] x_position = new float[2]{-13f, 13f}; public static float[] x_force = new float[8]{10f,12.5f,15f,20f,-10f,-12.5f,-15f,-20f}; //设定该飞碟工厂上游的场景控制器 public void SetDepend(SceneController firstCtrl){ this.myController = firstCtrl; } //检查一轮游戏是否结束 public bool CheckRoundFinish(){ for(int i = 0; i < trial; i++){ if(!UFOs[i].isOver){ return false; } } return true; } //当点击UFO时执行 public void ClickUFO(int id){ UFOs[id].SetUFOActive(false); myController.AddScore(); if(CheckRoundFinish()){ if(mCoroutine != null){ StopCoroutine(mCoroutine); mCoroutine = null; } myController.RoundDone(); } } //当UFO未被点击并离开视线时执行 public void FailUFO(int id){ UFOs[id].SetUFOActive(false); myController.SubBlood(); if(CheckRoundFinish()){ if(mCoroutine != null){ StopCoroutine(mCoroutine); mCoroutine = null; } myController.RoundDone(); } } //开始一轮游戏 public void StartRound(){ mCoroutine = StartCoroutine(SetUFOStart(2f-difficulty*0.2f)); } //让飞碟移动 IEnumerator SetUFOStart(float gapTime){ for(int i = 0; i < trial; i++){ int side = Random.Range(0,2); UFOs[i].SetUFOActive(true); UFOs[i].Fly(colors[Random.Range(0,5)], scales[Random.Range(0,5)]*(2f-difficulty*0.2f), new Vector3(x_position[side],y_position[Random.Range(0,5)],0), new Vector3(x_force[Random.Range(0,4)+side*4],0,0)); yield return new WaitForSeconds(gapTime); } } //设置难度 public void SetDifficulty(int difficulty){ this.difficulty = difficulty; } //在每轮游戏开始前初始化所有飞碟 public void InitializeUFO(){ if(mCoroutine != null){ StopCoroutine(mCoroutine); mCoroutine = null; } for(int i = 0; i < trial; i++){ if(UFOs[i] != null){ Destroy(UFOs[i].ufoObject); } UFOs[i] = new UFO(this, i); UFOs[i].SetUFOActive(false); } } }
在初始化UFO类时,先把Click和MoveOut两个脚本挂上去,通过Click()和Fail()函数接收来自这两个脚本的反馈信息。通过设置该游戏对象是否可见从而控制飞碟的生成和消失。只有当飞碟工厂命令该飞碟开始运动时才会把rigidbody组件附加到游戏对象上并且添加一个额外的力使得飞碟作法抛物线运动。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class UFO { int id; public bool isOver; public GameObject ufoObject; private UFOFatory myFatory; public UFO(UFOFatory fatory, int id){ this.myFatory = fatory; this.id = id; this.isOver = false; ufoObject = GameObject.Instantiate(Resources.Load("Prefabs/UFO", typeof(GameObject))) as GameObject; ufoObject.AddComponent<Click>(); ufoObject.GetComponent<Click>().SetDepend(this); ufoObject.AddComponent<MoveOut>(); ufoObject.GetComponent<MoveOut>().SetDepend(this); } //设置飞碟对象是否可见 public void SetUFOActive(bool boolean){ ufoObject.SetActive(boolean); } //处理飞碟点击事件 public void Click(){ this.isOver = true; myFatory.ClickUFO(this.id); } //该飞碟已经飞出屏幕时触发 public void Fail(){ this.isOver = true; myFatory.FailUFO(this.id); } //飞碟开始移动 public void Fly(Color c, float scale, Vector3 pos, Vector3 force){ ufoObject.SetActive(true); ufoObject.GetComponent<Renderer>().material.color = c; ufoObject.transform.localScale = ufoObject.transform.localScale * scale; ufoObject.transform.position = pos; if(!ufoObject.GetComponent<Rigidbody>()){ ufoObject.AddComponent<Rigidbody>(); } ufoObject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse); } }
Click脚本中设置监听鼠标事件并且制定了解决该鼠标事件的函数。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Click : MonoBehaviour { UFO ufo; //设置上层对象 public void SetDepend(UFO ufo) { this.ufo = ufo; } //设置点击事件 void OnMouseDown() { this.ufo.Click(); } }
MoveOut脚本中每一帧检查飞碟的位置并且判读是否已经超出视线范围,如果已经超过则调用飞碟类中的Fail函数解决该事件。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveOut : MonoBehaviour { UFO ufo; bool onScreen = true; Vector3 position; //设置上层对象 public void SetDepend(UFO ufo){ this.ufo = ufo; } //判断飞碟是否已经飞出屏幕外 void Update(){ if(!onScreen) return; this.position = ufo.ufoObject.transform.position; if(this.position.x < -15 || this.position.x > 15 || this.position.y < -8 || this.position.y > 8){ onScreen = false; ufo.Fail(); } } }
UserGUI负责绘制标题、按钮、显示得分和血量等。在游戏开始后,按钮变灰且不可以再被按下,只有当游戏结束时该按钮才重新可用。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class UserGUI : MonoBehaviour { SceneController CurrentSceneController; GUIStyle msgStyle, titleStyle; string roundText, bloodText, scoreText; void Start(){ CurrentSceneController = Director.GetInstance().CurrentSceneController; msgStyle = new GUIStyle(); msgStyle.normal.textColor = Color.black; msgStyle.alignment = TextAnchor.MiddleCenter; msgStyle.fontSize = 30; titleStyle = new GUIStyle(); titleStyle.normal.textColor = Color.black; titleStyle.alignment = TextAnchor.MiddleCenter; titleStyle.fontSize = 60; } void OnGUI(){ roundText = $"Round: {CurrentSceneController.round}"; bloodText = $"Blood: {CurrentSceneController.blood}"; scoreText = $"Score: {CurrentSceneController.score}"; GUI.enabled = !CurrentSceneController.isStart; // 重新开始的按钮 if(GUI.Button(new Rect(Screen.width*0.75f, Screen.height*0.85f, Screen.width*0.2f, Screen.height*0.1f), "Start")){ CurrentSceneController.StartNewGame(); } // 标题与其他提示信息 GUI.Label(new Rect(0, 0, Screen.width, Screen.height*0.2f), "Hit UFO", titleStyle); GUI.Label(new Rect(0, Screen.height*0.2f, Screen.width*0.15f, Screen.height*0.1f), roundText, msgStyle); GUI.Label(new Rect(0, Screen.height*0.3f, Screen.width*0.15f, Screen.height*0.1f), bloodText, msgStyle); GUI.Label(new Rect(0, Screen.height*0.4f, Screen.width*0.15f, Screen.height*0.1f), scoreText, msgStyle); //判断是否结束游戏 if(CurrentSceneController.isFailed){ GUI.Label(new Rect(0, Screen.height*0.4f, Screen.width, Screen.height*0.2f), "Game Over.", titleStyle); } } }
代码:https://gitee.com/GallonC/unityhomework/tree/master/homework4
演示:https://www.bilibili.com/video/BV1Tr4y117FW?spm_id_from=333.999.0.0
运行方式:新建一个unity3D项目,命名为 hit_UFO,将该新项目中的Assert文件替换为以上代码提供的Assert文件。在Unity软件中创建一个空游戏对象,将SceneController.cs挂到该空游戏对象上,将UserGUI挂到camera上。点击运行即可。