Unity Game Production – Priest and Devil Action Separate Edition

Table of Contents

I. Introduction

2. Code introduction

Model

View

Controller

Actions

Action event interface SSActionEvent

Action base class SSAction

Simple action implementation CCMoveToAction

Sequence action combination class CCSequenceAction

Action management base class SSActionManager

Using a combination of actions CCActionManager

FirstController

Judge class JudgeController

3. End


1. Foreword

This blog is an advanced version of the previous blog. The link to the previous blog is: Unity Game Production – Priests and Devils – CSDN Blog. This time it is mainly based on the code example provided by the teacher, based on the previous game:

  • Integrate CCAction to separate action management from the scene controller.
  • Using the interface message passing mechanism, design a referee class to notify the scene controller that the game is over when the game reaches the end condition. Separate the game end determination from the scene controller.

2. Code introduction

The specific difference between the second version of the game and the previous version is that the code part removes the movehe movecrtl class originally in the controller, and adds a series of Action classes and the referee class JudgeController.

The UML diagram after the modified structure is as follows:

The overall structure of the project is as follows:

The following will mainly introduce the new and modified parts.

Model

No change.

View

No changes, same as last version.

Controller

(It should be reasonable to include Actions in the control part…)

The changes are mainly in this part, which changes the original control structure.

In this part, the original move and moveController that control the movement of objects are deleted and replaced with Actions (although it seems more complicated in this game, if it is a larger game, the code structure will be clearer) , the division of labor is more clear). At the same time, the FirtstController was modified and the referee class JudgeController was added.

Actions

Action event interface SSActionEvent
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType:int {Started, Completed}
public interface ISSActionCallback
{
    //Callback
    void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Completed,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null);
}

The interface is an abstract type of object that receives notifications. It defines an event processing interface. All event managers must implement this interface to implement event scheduling. So, composite events need to implement it, and event managers must implement it too. What is to be implemented here is a callback function. The function is that when the action/event ends, the action/event tells the action/event manager whether the action/event has ended and let the manager strong>Modify the state instead of modifying the action itself to separate action management from events.

The subsequent code will reflect the specific implementation of this function. Here is how to write the function’s default parameters.

Action base class SSAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SSAction : ScriptableObject
{
    public bool enable = true;
    public bool destroy = false;

    public GameObject gameObject { get; set; }
    public Transform transform { get; set; }
    public ISSActionCallback callback { get; set; }

    protectedSSAction()
    {

    }

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

SSAction is the base class of actions, and subsequent actions must be derived from it. As a parent class, SSAction uses virtual to declare virtual methods and achieve polymorphism through overriding. This way the inheritor explicitly uses Start and Update to program game object behavior. The interface above ISSACtionCallback is used to implement message notification to avoid direct dependence on the action manager.

Simple action implementation CCMoveToAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCMoveToAction : SSAction
{
    //destination
    public Vector3 target;
    //speed
    public float speed;

    privateCCMoveToAction()
    {

    }

    public static CCMoveToAction GetSSAction(Vector3 target, float speed)
    {
        CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
        action.target = target;
        action.speed = speed;
        return action;
    }

    // Start is called before the first frame update
    public override void Start()
    {
        
    }

    // Update is called once per frame
    public override void Update()
    {
        //Determine whether the movement conditions are met
        if (this.gameObject == null || this.transform.localPosition == target)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this);
            return;
        }
        //move
        this.transform.localPosition = Vector3.MoveTowards(this.transform.localPosition, target, speed * Time.deltaTime);
    }
}

Implement a simple action: move an object to a target location at a specific speed, notify the task of completion, and expect the management program to automatically recycle the running object.

Sequential action combination class CCSequenceAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCSequenceAction : SSAction, ISSActionCallback
{
    //action sequence
    public List<SSAction> sequence;
    //repeat times
    public int repeat = -1;
    //Action start pointer
    public int start = 0;

    //Production Function
    public static CCSequenceAction GetSSAction(int repeat, int start, List<SSAction> sequence)
    {
        CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
        action.repeat = repeat;
        action.start = start;
        action.sequence = sequence;
        return action;
    }

    //Initialize the actions in the sequence
    public override void Start()
    {
        foreach (SSAction action in sequence)
        {
            action.gameObject = this.gameObject;
            action.transform = this.transform;
            action.callback = this;
            action.Start();
        }
    }

    //Run the actions in the sequence
    public override void Update()
    {
        if (sequence.Count == 0)
            return;
        if (start < sequence.Count)
        {
            sequence[start].Update();
        }
    }

    //Callback processing, triggered when an action is completed
    public void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Completed,
        int Param = 0,
        string strParam = null,
        Object objectParam = null)
    {
        source.destroy = false;
        this.start + + ;
        if (this.start >= sequence.Count)
        {
            this.start = 0;
            if (repeat > 0)
                repeat--;
            if (repeat == 0)
            {
                this.destroy = true;
                this.callback.SSActionEvent(this);
            }
        }
    }

    voidOnDestroy()
    {

    }
}

In this class, the action combination inherits the abstract action and can be further combined; it implements callback acceptance and can receive events of the combined action. In the overloaded GetSSAction, an action sequence is created, using -1 to indicate an infinite loop and 0 to start the action. Overload the Start and Update methods to initialize and execute the current action. The implemented interface SSActionEvent receives the completion of the current action and pushes the next action. If a cycle is completed, the number of times is reduced. If completed, notify the administrator of the action.

Action management base class SSActionManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SSActionManager : MonoBehaviour
{
    //Action set, exists in dictionary form
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    //Waiting for the action queue to be added (the action is about to start)
    private List<SSAction> waitingAdd = new List<SSAction>();
    //Action queue waiting to be deleted (action completed)
    private List<int> waitingDelete = new List<int>();

    protected void Update()
    {
        //Save the action in waitingAdd
        foreach (SSAction ac in waitingAdd)
            actions[ac.GetInstanceID()] = ac;
        waitingAdd.Clear();

        //Run the saved event
        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            if(ac.destroy)
            {
                waitingDelete.Add(ac.GetInstanceID());
            }else if (ac.enable)
            {
                ac.Update();
            }
        }

        //Destroy the action in waitingDelete
        foreach (int key in waitingDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            Destroy(ac);
        }
        waitingDelete.Clear();
    }

    //Prepare to run an action, initialize the action and add it to waitingAdd
    public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
    {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }

    // Start is called before the first frame update
    protected void Start()
    {

    }

}

This is the base class of the action object manager, which implements the basic management of all actions. This base class creates a MonoBehaiviour to manage a collection of actions. The actions are automatically recycled after the actions are completed, and an action dictionary is maintained. One question here is, the dictionary is thread-unsafe, will it affect our game? The answer is no, because there is only one thread in this game, and the action manager uses the isMoving parameter as a lock to deny the possibility of parallelism.

SSActionManger also provides the method RunAction to add a new action. This method binds the game object to the action and binds the message receiver of the action event.

Use action combination CCActionManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCActionManager : SSActionManager, ISSActionCallback
{
    //Whether it is moving
    private bool isMoving = false;
    //Ship movement action class
    public CCMoveToAction moveBoatAction;
    //Human movement action class (requires combination)
    public CCSequenceAction moveRoleAction;
    //Controller
    public FirstController controller;

    protected new void Start()
    {
        controller = (FirstController)Director.GetInstance().CurrentSceneController;
        controller.actionManager = this;
    }

    public bool IsMoving()
    {
        return isMoving;
    }

    //Move the boat
    public void MoveBoat(GameObject boat, Vector3 target, float speed)
    {
        if (isMoving)
            return;
        isMoving = true;
        moveBoatAction = CCMoveToAction.GetSSAction(target, speed);
        this.RunAction(boat, moveBoatAction, this);
    }

    //Move people
    public void MoveRole(GameObject role, Vector3 mid_destination, Vector3 destination, int speed)
    {
        if (isMoving)
            return;
        isMoving = true;
        moveRoleAction = CCSequenceAction.GetSSAction(0, 0, new List<SSAction> { CCMoveToAction.GetSSAction(mid_destination, speed), CCMoveToAction.GetSSAction(destination, speed) });
        this.RunAction(role, moveRoleAction, this);
    }

    //Callback
    public void SSActionEvent(SSAction source,
    SSActionEventType events = SSActionEventType.Completed,
    int intParam = 0,
    string strParam = null,
    Object objectParam = null)
    {
        isMoving = false;
    }
}

Real guy. Inherit the above action management base class and implement the callback function interface. The specific actions of moving boats and people are implemented. After the action is completed, the manager is notified through the callback function that the action has ended, isMoving=false. isMoving can be understood as a lock, which avoids the possibility of multiple actions at the same time.

FirstController

As the subject of control, FirstController will of course be modified:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class FirstController : MonoBehaviour, SceneController, UserAction
{
    public CCActionManager actionManager;
    public ShoreCtrl leftShoreController, rightShoreController;
    public river river;
    public BoatCtrl boatController;
    public RoleCtrl[] roleControllers;
    public bool isRunning;
    public float time;

    public void JudgeCallback(bool isRuning, string message)
    {
        this.gameObject.GetComponent<UserGUI>().gameMessage = message;
        this.gameObject.GetComponent<UserGUI>().time = (int)time;
        this.isRunning = isRunning;
    }

    public void LoadResources()
    {
        //role
        roleControllers = new RoleCtrl[6];
        for (int i = 0; i < 6; + + i)
        {
            roleControllers[i] = new RoleCtrl();
            roleControllers[i].CreateRole(Position.role_shore[i], i < 3 ? true : false, i);
        }

        //Load this shore and the other shore
        leftShoreController = new ShoreCtrl();
        leftShoreController.CreateShore(Position.left_shore);
        leftShoreController.GetShore().shore.name = "this_shore";
        rightShoreController = new ShoreCtrl();
        rightShoreController.CreateShore(Position.right_shore);
        rightShoreController.GetShore().shore.name = "other_shore";

        //Add and position the character to the left
        foreach (RoleCtrl roleController in roleControllers)
        {
            roleController.GetRoleModel().role.transform.localPosition = leftShoreController.AddRole(roleController.GetRoleModel());
        }
        //boat
        boatController = new BoatCtrl();
        boatController.CreateBoat(Position.left_boat);

        //river
        river = new River(Position.river);

        isRunning = true;
        time = 60;
    }

    public void MoveBoat()
    {
        if (isRunning == false || actionManager.IsMoving())
            return;

        Vector3 destination = boatController.GetBoatModel().isRight ? Position.left_boat : Position.right_boat;
        actionManager.MoveBoat(boatController.GetBoatModel().boat, destination, 5);

        boatController.GetBoatModel().isRight = !boatController.GetBoatModel().isRight;

    }

    public void MoveRole(Role roleModel)
    {
        if (isRunning == false || actionManager.IsMoving())
            return;
        Vector3 destination, mid_destination;
        if (roleModel.inBoat)
        {

            if (boatController.GetBoatModel().isRight)
                destination = rightShoreController.AddRole(roleModel);
            else
                destination = leftShoreController.AddRole(roleModel);
            if (roleModel.role.transform.localPosition.y > destination.y)
                mid_destination = new Vector3(destination.x, roleModel.role.transform.localPosition.y, destination.z);
            else
                mid_destination = new Vector3(roleModel.role.transform.localPosition.x, destination.y, destination.z);

            actionManager.MoveRole(roleModel.role, mid_destination, destination, 10);
            roleModel.onRight = boatController.GetBoatModel().isRight;
            boatController.RemoveRole(roleModel);
        }
        else
        {

            if (boatController.GetBoatModel().isRight == roleModel.onRight)
            {
                if (roleModel.onRight)
                {
                    rightShoreController.RemoveRole(roleModel);
                }
                else
                {
                    leftShoreController.RemoveRole(roleModel);
                }
                destination = boatController.AddRole(roleModel);
                if (roleModel.role.transform.localPosition.y > destination.y)
                    mid_destination = new Vector3(destination.x, roleModel.role.transform.localPosition.y, destination.z);
                else
                    mid_destination = new Vector3(roleModel.role.transform.localPosition.x, destination.y, destination.z);
                actionManager.MoveRole(roleModel.role, mid_destination, destination, 5);
            }
        }
    }

    public void Check(){ }

    public void RestartGame()
    {
        if (GUI.Button(new Rect(0, 35, 100, 30), "Restart"))
        {
            // Reload the game scene. There is only one scene, so the number is 0.
            SceneManager.LoadScene(0);
        }
    }

    voidAwake()
    {
        Director.GetInstance().CurrentSceneController = this;
        LoadResources();
        this.gameObject.AddComponent<UserGUI>();
        this.gameObject.AddComponent<CCActionManager>();
        this.gameObject.AddComponent<JudgeController>();
    }

    // Update is called once per frame
    void Update()
    {
        if(isRunning)
        {
            time -= Time.deltaTime;
            this.gameObject.GetComponent<UserGUI>().time = (int)time;
        }
    }
}

Compared with the previous version, the logic of controlling ships and characters has been mainly modified. The check function is extracted as a separate referee class.

Judge class JudgeController

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class JudgeController : MonoBehaviour
{
    public FirstController mainController;
    public Shore leftShoreModel;
    public Shore rightShoreModel;
    public Boat boatModel;
    
    // Start is called before the first frame update
    void Start()
    {
        mainController = (FirstController)Director.GetInstance().CurrentSceneController;
        this.leftShoreModel = mainController.leftShoreController.GetShore();
        this.rightShoreModel = mainController.rightShoreController.GetShore();
        this.boatModel = mainController.boatController.GetBoatModel();
    }

    // Update is called once per frame
    void Update()
    {
        if (!mainController.isRunning)
            return;
        if (mainController. time <= 0)
        {
            mainController.JudgeCallback(false, "Game Over!");
            return;
        }
        this.gameObject.GetComponent<UserGUI>().gameMessage = "";
        //Determine whether victory has been achieved
        if (rightShoreModel.pastorCount == 3)
        {
            mainController.JudgeCallback(false, "You Win!");
            return;
        }
        else
        {
            
            int leftPastorNum, leftDevilNum, rightPastorNum, rightDevilNum;
            leftPastorNum = leftShoreModel.pastorCount + (boatModel.isRight ? 0 : boatModel.pastorCount);
            leftDevilNum = leftShoreModel.devilCount + (boatModel.isRight ? 0 : boatModel.devilCount);
            if (leftPastorNum != 0 & amp; & amp; leftPastorNum < leftDevilNum)
            {
                mainController.JudgeCallback(false, "Game Over!");
                return;
            }
            rightPastorNum = rightShoreModel.pastorCount + (boatModel.isRight ? boatModel.pastorCount : 0);
            rightDevilNum = rightShoreModel.devilCount + (boatModel.isRight ? boatModel.devilCount : 0);
            if (rightPastorNum != 0 & amp; & amp; rightPastorNum < rightDevilNum)
            {
                mainController.JudgeCallback(false, "Game Over!");
                return;
            }
        }
    }
}

The referee class is implemented separately from the check function in the original FirstController, detects the game status, and notifies FirstController of the end of the game record through a callback at the end of the game, thus realizing the separation of the game end determination from the scene controller.

3. End

At this point, the modified project introduction is completed, the running method is the same as the previous version, and the running effect has not changed, so there will be no video demonstration.