Write a simple mouse flying saucer (Hit UFO) game
Game content requirements:
- The game has n round s, each containing 10 trial s;
- The color, size, launch position, speed, angle and number of simultaneous occurrences of each trial may vary. They are controlled by the round's ruler;
- The flying saucers of each trial are random, and the overall difficulty increases with the round.
- Score in the mouse point, the scoring rules are calculated according to different colors, sizes and speeds, and the rules can be set freely.
Game Requirements:
- Use cached factory mode to manage the production and recycling of different flying saucers, which must be a single instance of the scene! See the reference resource Singleton template class for implementation
- Separate human-computer interaction from game models using the previous MVC structure whenever possible
Rules of the game
- The game is divided into 3 rounds, each player has 5 lives, one life is deducted for each missing flying disc, otherwise, the corresponding score can be obtained by one flying disc per mouse point.
- The difficulty is increased by turns, and the flying speed of the flying saucer is faster with the increase of turns.
- Color, size of each trail's flying saucer; The launch position, speed, angle, and number of flying saucers per launch vary;
_Because an object instance is created every time a flying saucer appears, the destruction of the flying saucer requires the destruction of the instance, which greatly increases the cost of running the game, so use factory mode. The flying saucer factory manages the creation and recycling of prefabricated flying saucers separately. The flying saucer factory first creates a stack of flying saucers of different colors, then randomly removes a usable flying saucer from the factory when it is needed. When the flying saucer is not needed, it is placed in the factory, eliminating the cost of creating and destroying instances of flying saucers.
The UML diagram of the game project is as follows
Game Realization
DiskFactory
The flying saucer factory class contains the generation and recycling of flying saucers, as well as settings that adjust the speed of the flying saucer according to the turn. A list is used to generate and destroy flying saucers. First, an instance of the flying saucer is created and placed in the free queue. Then, when the flying saucer is needed, it is fetched from the free queue and the used flying saucer is placed in the user queue.
public class DiskFactory : MonoBehaviour { public GameObject disk_prefab = null; //Prefabricated flying saucer private List<DiskData> used = new List<DiskData>(); //List of flying saucers in use private List<DiskData> free = new List<DiskData>(); //Free list of flying saucers public GameObject GetDisk(int round) { int choice = 0; int scope1 = 1, scope2 = 4, scope3 = 7; //Random Range float start_y = -10f; //Vertical position of the flying saucer just instantiated string tag; disk_prefab = null; //Randomly select flying saucers to fly out based on rounds if (round == 1) { choice = Random.Range(0, scope1); } else if(round == 2) { choice = Random.Range(0, scope2); } else { choice = Random.Range(0, scope3); } //tag of the flying saucer to be selected if(choice <= scope1) { tag = "disk1"; } else if(choice <= scope2 && choice > scope1) { tag = "disk2"; } else { tag = "disk3"; } //Find a free flying saucer with the same tag for(int i=0;i<free.Count;i++) { if(free[i].tag == tag) { disk_prefab = free[i].gameObject; free.Remove(free[i]); break; } } //Reinstantiate the flying saucer if it is not in the free list if(disk_prefab == null) { if (tag == "disk1") { disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity); } else if (tag == "disk2") { disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity); } else { disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity); } //Give the newly instantiated flying saucer additional attributes float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1; disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<DiskData>().color; disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0); disk_prefab.transform.localScale = disk_prefab.GetComponent<DiskData>().scale; } //Add to Usage List used.Add(disk_prefab.GetComponent<DiskData>()); return disk_prefab; } //Recycle flying saucer public void FreeDisk(GameObject disk) { for(int i = 0;i < used.Count; i++) { if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID()) { used[i].gameObject.SetActive(false); free.Add(used[i]); used.Remove(used[i]); break; } } } }
DiskData
Diskdata, a data class used to provide attributes to a flying saucer, the score you get when you click on it, the color of the flying saucer, and the speed and direction at which the flying saucer flies.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DiskData : MonoBehaviour { //Shoot this flying saucer score public int score = 1; //colour public Color color = Color.red; //speed public float speed = 20; //direction public Vector3 direction; }
RoundController
RoundController, a scene controller, and a scene controller are responsible for the overall management of each component of a game project. The game is divided into three states: the beginning of the game, the ongoing game, and the end of the game. Depending on the game, invoke the interface with the UFO Communist so that the difficulty of the UFO game matches the game rounds. The scene controller surges into the timer and sends a flying saucer after a certain interval, updating its life value whenever a player misses a flying saucer.
public class RoundController : MonoBehaviour, ISceneController, IUserAction { public DiskFactory diskFactory; public CCActionManager actionManager; public ScoreRecorder scoreRecorder; public UserGUI userGui; private Queue<GameObject> diskQueue = new Queue<GameObject>(); private List<GameObject> diskMissed = new List<GameObject>(); private int totalRound = 3; private int trialNumPerRound = 10; private int currentRound = -1; private int currentTrial = -1; private float throwSpeed = 2f; private int gameState = 0; //-1: Failure 0: Initial state 1: In progress 2: Win private float throwInterval = 0; private int userBlood = 10; void Awake() { SSDirector director = SSDirector.GetInstance(); director.CurrentSceneController = this; diskFactory = Singleton<DiskFactory>.Instance; userGui = gameObject.AddComponent<UserGUI>() as UserGUI; actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager; scoreRecorder = new ScoreRecorder(); } public void LoadResource() { diskQueue.Enqueue(diskFactory.GetDisk(currentRound)); } public void ThrowDisk(int count) { while(diskQueue.Count <= count) { LoadResource(); } for(int i = 0; i < count; i++) { float position_x = 16; GameObject disk = diskQueue.Dequeue(); diskMissed.Add(disk); disk.SetActive(true); //Set the position of the flying saucer float ran_y = Random.Range(-3f, 3f); float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1; disk.GetComponent<DiskData>().direction = new Vector3(ran_x, ran_y, 0); Vector3 position = new Vector3(-disk.GetComponent<DiskData>().direction.x * position_x, ran_y, 0); disk.transform.position = position; //Set the initial force and angle of the flying saucer float power = Random.Range(10f, 15f); float angle = Random.Range(15f, 28f); actionManager.diskFly(disk, angle, power); } } void levelUp() { currentRound += 1; throwSpeed -= 0.5f; currentTrial = 1; } void Update() { if(gameState == 1) { if(userBlood <= 0 || (currentRound == totalRound && currentTrial == trialNumPerRound)) { GameOver(); return; } else { if (currentTrial > trialNumPerRound) { levelUp(); } if (throwInterval > throwSpeed) { int throwCount = generateCount(currentRound); ThrowDisk(throwCount); throwInterval = 0; currentTrial += 1; } else { throwInterval += Time.deltaTime; } } } for (int i = 0; i < diskMissed.Count; i++) { GameObject temp = diskMissed[i]; //The flying saucer flew out of the camera's view and was not hit if (temp.transform.position.y < -8 && temp.gameObject.activeSelf == true) { diskFactory.FreeDisk(diskMissed[i]); diskMissed.Remove(diskMissed[i]); userBlood -= 1; } } } public int generateCount(int currentRound) { if(currentRound == 1) { return 1; } else if(currentRound == 2) { return Random.Range(1, 2); } else { return Random.Range(1, 3); } } public void StartGame() { gameState = 1; currentRound = 1; currentTrial = 1; userBlood = 10; throwSpeed = 2f; throwInterval = 0; } public void GameOver() { if(userBlood <= 0) { gameState = -1;//fail } else { gameState = 2;//victory } } public void Restart() { scoreRecorder.Reset(); StartGame(); } public void Hit(Vector3 pos) { Ray ray = Camera.main.ScreenPointToRay(pos); RaycastHit[] hits; hits = Physics.RaycastAll(ray); bool notHit = false; foreach (RaycastHit hit in hits) //Radiation hit object if (hit.collider.gameObject.GetComponent<DiskData>() != null) { //Objects to be shot in the list of flying saucers not hit for (int j = 0; j < diskMissed.Count; j++) { if (hit.collider.gameObject.GetInstanceID() == diskMissed[j].gameObject.GetInstanceID()) { notHit = true; } } if (!notHit) { return; } diskMissed.Remove(hit.collider.gameObject); //Record score scoreRecorder.Record(hit.collider.gameObject); diskFactory.FreeDisk(hit.collider.gameObject); break; } } public int GetScore() { return scoreRecorder.GetScore(); } public int GetCurrentRound() { return currentRound; } public int GetBlood() { return userBlood; } public int GetGameState() { return gameState; } }
Game interface
The game interface is used to update the game and the player's data, including score, life, current round, and so on.
public class UserGUI : MonoBehaviour { private IUserAction action; private string score, round; int blood, gameState, HighestScore; // Start is called before the first frame update void Start() { action = SSDirector.GetInstance().CurrentSceneController as IUserAction; } // Update is called once per frame void Update() { gameState = action.GetGameState(); } void OnGUI() { GUIStyle text_style; GUIStyle button_style; text_style = new GUIStyle() { fontSize = 20 }; button_style = new GUIStyle("button") { fontSize = 15 }; if (gameState == 0) { //Initial interface if (GUI.Button(new Rect(Screen.width / 2 - 50, 80, 100, 60), "Start Game", button_style)) { action.StartGame(); } } else if(gameState == 1) { //The game is in progress //User Shooting if (Input.GetButtonDown("Fire1")) { Vector3 mousePos = Input.mousePosition; action.Hit(mousePos); } score = "Score: " + action.GetScore().ToString(); GUI.Label(new Rect(200, 5, 100, 100), score, text_style); round = "Round: " + action.GetCurrentRound().ToString(); GUI.Label(new Rect(400, 5, 100, 100), round, text_style); blood = action.GetBlood(); string bloodStr = "Blood: " + blood.ToString(); GUI.Label(new Rect(600, 5, 50, 50), bloodStr, text_style); } else { //There are two situations when the game ends if (gameState == 2) { if (action.GetScore() > HighestScore) { HighestScore = action.GetScore(); } GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 250, 100, 60), "Game Over", text_style); string record = "Highest Score: " + HighestScore.ToString(); GUI.Label(new Rect(Screen.width / 2 - 70, Screen.height / 2 - 150, 150, 60), record, text_style); } else { GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 150, 100, 70), "You Lost!", text_style); } if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 30, 100, 60), "Restart", button_style)) { action.Restart(); } } } }
FlyAction
Flying saucer action class, mainly responsible for achieving the flying effect of the saucer, including giving the saucer a direction and force, making the saucer do a movement with similar gravity acceleration, which will stop when the saucer flies out of the game interface.
public class FlyAction : SSAction { public float gravity = -5; //Downward acceleration private Vector3 start_vector; //Initial Velocity Vector private Vector3 gravity_vector = Vector3.zero; //Vector of acceleration, initially 0 private float time; //Time Past private Vector3 current_angle = Vector3.zero; //Euler's angle of current time private UFOFlyAction() { } public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power) { //Initialize the initial velocity vector that the object will move UFOFlyAction action = CreateInstance<UFOFlyAction>(); if (direction.x == -1) { action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power; } else { action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power; } return action; } public override void Update() { //Calculates the downward speed of an object, v=at time += Time.fixedDeltaTime; gravity_vector.y = gravity * time; //Displacement simulation transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime; current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg; transform.eulerAngles = current_angle; //If the y-coordinate of the object is less than -10, the action is complete if (this.transform.position.y < -10) { this.destroy = true; this.callback.SSActionEvent(this); } } public override void Start() { } }
Effect Display
Start Interface
Game in progress interface
Game End Interface