Unity realizes flying saucer games
Adapter mode overview
definition
The adapter pattern converts the interface of a class into another interface expected by the client. The main purpose is compatibility, so that the two classes that cannot work together due to interface mismatch can work together. Its alias is wrapper, which belongs to structural mode.
For convenience of expression, definition:
- The classes, interfaces, and objects to be adapted are called src (source) for short
- The final required output is referred to as dst (destination, i.e. Target)
- An Adapter is called an Adapter.
One sentence describes the feeling of Adapter mode: src - > Adapter - > dst, that is, src is given to the Adapter in some form (three forms correspond to three Adapter modes respectively) and finally transformed into dst.
Usage scenario
- The system needs to use existing classes, and the interfaces of these classes do not meet the needs of the system.
- You want to create a reusable class to work with some classes that are not closely related to each other, including some classes that may be introduced in the future.
- A unified output interface is required, and the type of input is unpredictable.
classification
- The class Adapter is given by class. In the Adapter, src is inherited as a class,
- The object Adapter is given by object. In the Adapter, src is held as an object.
- The interface Adapter is given by the interface. In the Adapter, src is implemented as an interface.
Project requirements
Write a simple mouse flying saucer (Hit UFO) game
- Game content requirements:
- The game has n rounds, and each round includes 10 trial s;
- The color, size, launch position, speed, angle and number of UFOs in each trial may be different. They are controlled by the ruler of the round;
- The flying saucer of each trial has randomness, 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. The rules can be set freely.
- Basic game requirements:
- Use the factory mode with cache to manage the production and recycling of different UFOs. The factory must be a single instance of the scene! See the Singleton template class of the reference resource for specific implementation
- As far as possible, use the previous MVC structure to realize the separation of human-computer interaction and game model
- Requirements for adapter version games:
- Modify the UFO game according to the design drawing of adapter mode
- Make it support both physical motion and kinematic (transformation) motion
Project configuration
- Create a new project and replace the Assets file with the Assets file in my project. Since the operations in Chapter 5 and Chapter 6 are merged, there is one asset under the basic version and the sports and physical compatibility version folders respectively
- The first skybox fs000 in Assets/Resources/Fantasy Skybox FREE/Materials/Classic_ Day_ 01 drag to Scene
- Drag DiskFactory, RoundController and ScoreRecorder from Assets/Resources/Scripts to Main Camera
- Compile and run to start the game
Core algorithm analysis
Basic version (Ray only, action management)
The project framework is as follows:
DiskData.cs specifies that the UFO has the following properties:
public int score = 1; //Score points for shooting this flying saucer public Color color = Color.white; //UFO color public Vector3 direction; //Initial position of flying saucer public Vector3 scale = new Vector3( 1 ,0.25f, 1); //UFO size
MyDiskEditor.cs is used to make prefabricated and realize the graphical interface editing of UFO attributes:
The specific codes are as follows:
using UnityEngine; using UnityEditor; using System.Collections; [CustomEditor(typeof(DiskData))] [CanEditMultipleObjects] public class MyDEditor : Editor { SerializedProperty score; //fraction SerializedProperty color; //colour SerializedProperty scale; //size void OnEnable() { //Get individual values after serializing objects score = serializedObject.FindProperty("score"); color = serializedObject.FindProperty("color"); scale = serializedObject.FindProperty("scale"); } public override void OnInspectorGUI() { //Turn on update serializedObject.Update(); //Set slider EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score")); if (!score.hasMultipleDifferentValues) { //Show progress bar ProgressBar(score.intValue / 5f, "score"); } //Display value EditorGUILayout.PropertyField(color); EditorGUILayout.PropertyField(scale); //Apply updates serializedObject.ApplyModifiedProperties(); } private void ProgressBar(float value, string label) { Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField"); EditorGUI.ProgressBar(rect, value, label); //Leave a blank line in the middle EditorGUILayout.Space(); } }
RoundController.cs is a controller in MVC structure and a scene controller. It has the following member variables
public FlyActionManager action_manager; public DiskFactory disk_factory; public UserGUI user_gui; public ScoreRecorder score_recorder; private Queue<GameObject> disk_queue = new Queue<GameObject>(); //UFO queue in game scene private List<GameObject> disk_notshot = new List<GameObject>(); //Flying saucer queue not hit private int round = 1; //round private int trial = 1; private float interval = 2.1f; //The time interval between launching a UFO private bool playing_game = false; //In game private bool game_over = false; //game over private bool game_start = false; //The game begins
The following methods are implemented
//initialization void Start (); //For each frame update, regularly call the LoadResources function to notify the UFO processing factory to produce UFOs and call the launch UFO function void Update (); //Update trial and round and generate UFO interval public void UpdateTrial(); //Inform the UFO processing factory to produce UFOs and join the UFO queue public void LoadResources(); //Launch UFO private void SendDisk(); //Handle click events public void Hit(Vector3 pos); //Score public int GetScore(); //Get round public int GetRound(); //Get trial public int GetTrail(); //restart public void ReStart(); //Set the end of the game public void GameOver(); //Pause for a few seconds and recover the UFO IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj); //Start the game public void BeginGame();
Some key functions are implemented as follows
//For each frame update, regularly call the LoadResources function to notify the UFO processing factory to produce UFOs, and call the send UFO function void Update () { if(game_start) { //When the game is over, cancel sending UFOs regularly if (game_over) { CancelInvoke("LoadResources");//Cancel calling LoadResources } //Set a timer, send the UFO, and the game begins if (!playing_game) { InvokeRepeating("LoadResources", 1f, interval);//After 1 second, call LoadResources and call once every speed seconds. playing_game = true; } //Launch UFO SendDisk(); } } //Launch UFO private void SendDisk() { float position_x = 9; if (disk_queue.Count != 0) { GameObject disk = disk_queue.Dequeue(); disk_notshot.Add(disk); disk.SetActive(true); //Set the location of hidden or newly created flying saucers float ran_y = Random.Range(3f, 6f); 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, and the speed of the flying saucer generally increases with the increase of round. float power = Random.Range(0.9f + 0.1f * round, 1.35f + 0.15f * round); float angle = Random.Range(15f, 28f); action_manager.UFOFly(disk,angle,power,round); } for (int i = 0; i < disk_notshot.Count; i++) { GameObject temp = disk_notshot[i]; //The UFO flew out of view of the camera and was not hit if (temp.transform.position.y < -3 && temp.gameObject.activeSelf == true) { if(user_gui.life>1)UpdateTrial(); disk_factory.FreeDisk(disk_notshot[i]); disk_notshot.Remove(disk_notshot[i]); //Player HP - 1 user_gui.ReduceBlood(); } } } public void Hit(Vector3 pos) { Ray ray = Camera.main.ScreenPointToRay(pos); RaycastHit[] hits; hits = Physics.RaycastAll(ray); bool not_hit = false; for (int i = 0; i < hits.Length; i++) { RaycastHit hit = hits[i]; //The ray hit the object if (hit.collider.gameObject.GetComponent<DiskData>() != null) { //The hit object should be in the list of missed UFOs for (int j = 0; j < disk_notshot.Count; j++) { if (hit.collider.gameObject.GetInstanceID() == disk_notshot[j].gameObject.GetInstanceID()) { not_hit = true; } } if(!not_hit) { return; } UpdateTrial(); disk_notshot.Remove(hit.collider.gameObject); //The scorer records the score score_recorder.Record(hit.collider.gameObject); //Show explosive particle effects Transform explode = hit.collider.gameObject.transform.GetChild(0); explode.GetComponent<ParticleSystem>().Play(); //Wait 0.1 seconds to recover the UFO StartCoroutine(WaitingParticle(0.08f, hit, disk_factory, hit.collider.gameObject)); break; } } } //Pause for a few seconds and recover the UFO IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj) { yield return new WaitForSeconds(wait_time); //Action to be performed after waiting hit.collider.gameObject.transform.position = new Vector3(0, -9, 0); disk_factory.FreeDisk(obj); }
DiskFactory.cs is a flying saucer factory, which implements two methods: producing flying saucers and recycling flying saucers. The specific implementation is as follows:
public class DiskFactory : MonoBehaviour { public GameObject disk_prefab = null; //UFO preform private List<DiskData> used = new List<DiskData>(); //List of UFOs in use private List<DiskData> free = new List<DiskData>(); //Free UFO list public GameObject GetDisk(int round) { int choice = 0; int scope1 = 1, scope2 = 4, scope3 = 7; //Random range float start_y = -10f; //The vertical position of the flying saucer when it was just instantiated string tag; disk_prefab = null; //Randomly select the flying saucer to fly out choice = Random.Range(0, scope3); //The 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 free UFOs 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; } } //If not in the free list, re instantiate the UFO 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 UFO other 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; } //Recovery UFO 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; } } } }
ScoreRecorder.cs is a scorer, responsible for recording scores and resetting scores. The specific implementation is as follows:
public class ScoreRecorder : MonoBehaviour { public int score; //fraction void Start () { score = 0; } //Record score public void Record(GameObject disk) { int temp = disk.GetComponent<DiskData>().score; score = temp + score; } //Reset score public void Reset() { score = 0; } }
Singleton.cs is used in RoundController.cs to ensure that the UFO factory and scorer are in singleton mode. The specific implementation is as follows:
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; } } }
``
FlyActionManager.cs is the action manager of flying saucer flight. It uses the auxiliary class UFOFlyAction.cs to calculate the flying trajectory of flying saucer and control flying saucer. The specific implementation is as follows:
public class FlyActionManager : SSActionManager { public UFOFlyAction fly; //The act of flying a UFO public RoundController scene_controller; //Scene controller for the current scene protected void Start() { scene_controller = (RoundController)SSDirector.GetInstance().CurrentScenceController; scene_controller.action_manager = this; } //Flying saucer public void UFOFly(GameObject disk, float angle, float power, int round) { fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power); fly.gravity = -0.04f - 0.01f * round; this.RunAction(disk, fly, this); } } public class UFOFlyAction : SSAction { public float gravity =(float) -0.05; //Downward acceleration private Vector3 start_vector; //Initial velocity vector private Vector3 gravity_vector = Vector3.zero; //The vector of acceleration, initially 0 private float time; //Time has passed private Vector3 current_angle = Vector3.zero; //Euler angle of current time private UFOFlyAction() { } public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power) { //Initializes the initial velocity vector of the object to move UFOFlyAction action = CreateInstance<UFOFlyAction>(); if (direction.x < 0) { 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() { //Calculate the downward velocity of the 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 - 3, the action is completed if (this.transform.position.y < -3) { this.destroy = true; this.callback.SSActionEvent(this); } } public override void Start() { } }
Adapter Version (sports and physical compatibility)
The project framework is as follows:
To implement the adapter mode, the basic version is modified as follows:
- Added script:
- ActionManagerAdapter.cs is responsible for receiving the notification from the action manager and selecting whether to use the kinematic or physical motion interface to control the flying saucer flight. The specific implementation is as follows:
public class ActionManagerAdapter : MonoBehaviour,IActionManager { public FlyActionManager action_manager; public PhysisFlyActionManager phy_action_manager; public void playDisk(GameObject disk, float angle, float power,int round,bool isPhy) { if(isPhy) { phy_action_manager.UFOFly(disk, angle, power,round); } else { action_manager.UFOFly(disk, angle, power,round); } } // Use this for initialization void Start () { action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager; phy_action_manager = gameObject.AddComponent<PhysisFlyActionManager>() as PhysisFlyActionManager; } }
- PhysisFlyActionManager.cs uses physical motion to control flying saucers. The specific implementation is as follows:
public class PhysisFlyActionManager : SSActionManager { public PhysisUFOFlyAction fly; //The act of flying a UFO protected void Start() { } //Flying saucer public void UFOFly(GameObject disk, float angle, float power,int round) { fly = PhysisUFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power); fly.round=round; this.RunAction(disk, fly, this); } }
- Physicufoflyaction.cs uses motion physics to calculate the flying track of the flying saucer. Because the gravity acceleration is too large, the flying saucer falls too fast and is too difficult, it is necessary to add a continuous vertical acceleration to the flying saucer. Changing this acceleration can make the flying saucer have a downward acceleration that increases with the increase of the round. The specific implementation is as follows:
public class PhysisUFOFlyAction : SSAction { private Vector3 start_vector; //Initial velocity vector public float power; public int round; private PhysisUFOFlyAction() { } public static PhysisUFOFlyAction GetSSAction(Vector3 direction, float angle, float power) { //Initializes the initial velocity vector of the object to move PhysisUFOFlyAction action = CreateInstance<PhysisUFOFlyAction>(); 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; } action.power = power; return action; } public override void FixedUpdate() { //Determine whether it is out of range if (this.transform.position.y < -3) { this.destroy = true; this.callback.SSActionEvent(this); } } public override void Update() { } public override void Start() { //Use gravity plus an upward acceleration to give the UFO a downward acceleration that increases with the increase of round gameobject.GetComponent<Rigidbody>().useGravity = true; gameobject.GetComponent<Rigidbody>().AddForce(new Vector3(0, 9.77f - 0.01f * round, 0),ForceMode.Acceleration); //Give the UFO an initial speed gameobject.GetComponent<Rigidbody>().velocity = power * 7 * start_vector; } }
- ActionManagerAdapter.cs is responsible for receiving the notification from the action manager and selecting whether to use the kinematic or physical motion interface to control the flying saucer flight. The specific implementation is as follows:
- Modified script:
- FlyActionManager.cs removes the use of scene controller, and the modified code is as follows:
public class FlyActionManager : SSActionManager { public UFOFlyAction fly; //The act of flying a UFO protected void Start() { } //Flying saucer public void UFOFly(GameObject disk, float angle, float power, int round) { fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power); fly.gravity = -0.04f - 0.01f * round; this.RunAction(disk, fly, this); } }
- Interface.cs adds a kinematics and physical motion interface. The specific implementation is as follows:
//Kinematic and physical motion interface public interface IActionManager { void playDisk(GameObject disk, float angle, float power,int round,bool isPhy); }
- RoundController.cs adds a new member variable isPhy and changes the call to the flying saucer method managed by the action manager. Note that the action must be modified_ The type of manager is iationmanager. The specific implementation is as follows:
public bool isPhy = true; //Use physical motion //First set the action_ The type of manager is modified to iationmanager, and the invocation of instantiated methods is modified action_manager = gameObject.AddComponent<ActionManagerAdapter>() as IActionManager; //Action manager manages UFO flight method calls action_manager.playDisk(disk, angle, power,round,isPhy);
- SSActionManager.cs adds a physical motion update method. The specific implementation is as follows
protected void FixedUpdate() { 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) { //Physical motion update ac.FixedUpdate(); } } foreach (int key in waitingDelete) { SSAction ac = actions[key]; actions.Remove(key); DestroyObject(ac); } waitingDelete.Clear(); }
- UFOFlyAction.cs adds an empty fixed update method:
public override void FixedUpdate() { }
- FlyActionManager.cs removes the use of scene controller, and the modified code is as follows:
Effect display
Rules of the game
- The game has an infinite number of rounds, and each round includes 10 trial s;
- Click the mouse to score the flying saucer. The red flying saucer gets 3 points, the green flying saucer gets 2 points, and the purple flying saucer gets 1 point. The red flying saucer has the smallest volume and the purple flying saucer has the largest volume;
- If the score of each trial is less than 50% of the total score of flying saucers in this trial, the life is - 1. If the life > 0 after the end of the round, the next round will be entered, otherwise start again from round 1;
- The color, size, launch position, speed, angle and number of UFOs in each trial are random. The overall difficulty increases with the round, that is, the speed of UFOs increases as a whole.
Game screenshot
The initial interface is as follows, showing the rules of the game:
The screenshot of the game is as follows. The score, round number, trial number and life value are displayed at the top of the interface:
There are explosion effects when hitting a flying saucer:
When the HP is exhausted, the game ends, and the maximum number of recorded rounds and maximum scores are displayed:
experience
- Deepen the understanding of the design of action manager
- Deepen the understanding of factory mode and single instance mode
- Further practice the separation of human-computer interaction and game model using MVC
- Learned about adapter patterns
- Practiced using the physics engine for rigid body dynamics