Unity realizes flying saucer games

Posted by bh on Sat, 23 Oct 2021 08:43:16 +0200

Unity realizes flying saucer games

Project address

Demo video

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;
          }
      }
      
  • 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() { }
      

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

Topics: C# Unity