The Devil and the Priest (action separated version)

Table of Contents

Preface

Results display and acquisition

action separation

Action separation design

Design ideas

action design

ActionController

callback function interface

action base class

single action

Combination moves

Action manager base class

Example of this game action manager

Referee

Model design

interface design

click script

User interface scripts


Foreword

Based on the basic idea of the MVC version of the previous game Devil and Priest, a Devil and Priest game is made with general actions separated. Therefore, the analysis of the game itself is consistent with the previous game, only the code ideas are different.

For specific game analysis, please see:

http://t.csdnimg.cn/MOzvK

Achievements Display and Acquisition

Display: https://www.bilibili.com/video/BV1wG411C7dr/?share_source=copy_web & amp;vd_source=9d9b1417790062ff0fa21135bfdeae10

Get: https://gitee.com/Wesley-L/the-devil-and-the-pastor-action-separate.git

Action separation

In game design, action separation is a technique and method used to separate actions in a game from the state of a character or object. It can help developers better control and manage in-game actions, providing more flexible and scalable gameplay.

The basic idea of action separation is to define the actions of a character or object as independent action resources, rather than directly embedding them in the state of the character or object. These motion assets can be pre-recorded animation clips or procedurally generated animations. In this way, developers can combine and control these action resources as needed to achieve richer and more diverse game actions.

The benefits of action separation include:

  1. Flexibility: By separating actions from character states, developers can more flexibly combine and switch between different actions, allowing for more diverse gameplay and interaction methods.

  2. Scalability: Action separation makes it easier to add new actions. Developers can simply add new action resources without modifying existing game logic and code.

  3. Reusability: Because actions are defined and managed independently, they can be shared and reused among different roles or objects, thereby improving development efficiency and resource utilization.

  4. Fine control: Action separation allows developers to more finely control the details of actions, such as animation transitions, playback speed, looping methods, etc., to achieve a better gaming experience.

In practical applications, action separation usually requires the use of appropriate tools and technologies, such as animation editors, action state machines, etc. Developers need to design and define the character’s status and transition conditions, and associate the corresponding action resources with them. Through reasonable state management and action scheduling, smooth and realistic game actions can be achieved.

Action separation design

Design Idea

Action Design

ActionController

Callback function interface

The interface is used here to represent the operation that the caller will perform after each action is completed. This operation is placed in the callback function.

public enum SSActionEventType : int { Started, Competeted }
?
//interface
public interface ISSActionCallback {
    void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null);
}
Action Base Class

Here, the operations that need to be implemented by the subclass are specified through the base class and virtual functions, and the actions are controlled through the two variables “can be performed” and “can be deleted”.

//Action base class
public class SSAction : ScriptableObject {
    public bool enable = true; //Whether it is possible
    public bool destroy = false; //Whether it has been completed
?
    public GameObject gameobject; //action object
    public Transform transform; //transform of action object
    public ISSActionCallback callback; //Callback function
?
    /*Prevent users from creating their own new objects*/
    protected SSAction() { }
?
    public virtual void Start() {
        throw new System.NotImplementedException();
    }
?
    public virtual void Update() {
        throw new System.NotImplementedException();
    }
}
Single action

Note the following two operations of a single action:

//Here we use Unity’s own constructor to create the object and initialize it
public static SSMoveToAction GetSSAction(Vector3 _target, float _speed) {
    SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();
    action.target = _target;
    action.speed = _speed;
    return action;
}
?
public override void Update() {
    this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
    //The action is completed, notify the action manager or action combination
    if (this.transform.position == target) {
        this.destroy = true;
        this.callback.SSActionEvent(this);
    }
}
combination action

Combination actions include:

public List<SSAction> sequence; //Action list
public int repeat = -1; //-1 is an infinite loop
public int start = 0; //The label of the current action

Pay attention to the following functions:

//Let each action in the sequence be executed
public override void Update() {
    if (sequence.Count == 0) return;
    if (start < sequence.Count) {
        sequence[start].Update(); //After execution, increment the start value through the callback function
    }
}
?
//Callback
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null) {
    source.destroy = false; //Since it may be called again, do not delete it yet
    this.start + + ;
    if (this.start >= sequence.Count) {
        this.start = 0;
        if (repeat > 0) repeat--;
        if (repeat == 0) {
            this.destroy = true; //Delete
            this.callback.SSActionEvent(this); //Notify the manager
        }
    }
}
Action Manager Base Class

The action manager is a facade that can control actions by itself and provide interfaces for external function calls. Pay special attention to the queues waiting to be added and those waiting to be deleted. Since it is impossible to process new actions and executed actions immediately during each update, a queue is needed for buffering. Deleted action objects are also operated by Unity’s Destroy. For the facade mode, the function RunAction() is provided here. The outside world only needs to specify the game object, action and callback handler, and this class can complete the implementation of these actions.

public class SSActionManager : MonoBehaviour {
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //Action dictionary
    private List<SSAction> waitingAdd = new List<SSAction>(); //Action list waiting to be executed
    private List<int> waitingDelete = new List<int>(); //List of keys waiting to be deleted
?
    protected void Update() {
        //Add the actions waiting to be executed to the dictionary and clear the to-be-executed list
        foreach (SSAction ac in waitingAdd) {
            actions[ac.GetInstanceID()] = ac;
        }
        waitingAdd.Clear();
?
        //For each action in the dictionary, see whether it is executed or deleted
        foreach (KeyValuePair<int, SSAction> kv in actions) {
            SSAction ac = kv.Value;
            if (ac.destroy) {
                waitingDelete.Add(ac.GetInstanceID());
            }
            else if (ac.enable) {
                ac.Update();//It may be the execution of a combination of actions or a single action.
            }
        }
?
        //Delete all completed actions and clear the list to be deleted
        foreach (int key in waitingDelete) {
            SSAction ac = actions[key];
            actions.Remove(key);
            Object.Destroy(ac);//Let Unity help delete it
        }
        waitingDelete.Clear();
    }
?
    //The outside world only needs to call the RunAction function of the action management class to complete the action.
    public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) {
        action.gameobject = gameobject;
        action.transform = gameobject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }
}
This game action manager instance

Note that the moving boat and moving object are encapsulated here. Now the user only needs to provide the object, end point and speed.

public void moveBoat(GameObject boat, Vector3 end, float speed) {
    boatMove = SSMoveToAction.GetSSAction(end, speed);
    this.RunAction(boat, boatMove, this);
}
?
public void moveRole(GameObject role, Vector3 middle, Vector3 end, float speed) {
    //Two segments of movement
    SSAction action1 = SSMoveToAction.GetSSAction(middle, speed);
    SSAction action2 = SSMoveToAction.GetSSAction(end, speed);
    //Both actions are repeated only once
    roleMove = SequenceAction.GetSSAcition(1, 0, new List<SSAction> { action1, action2 });
    this.RunAction(role, roleMove, this);
}

Referee Class

Just move the original judgment function into the class and make some variable modifications:

public class GameCheck : MonoBehaviour {
    public Controller sceneController;
?
    protected void Start() {
        sceneController = (Controller)SSDirector.GetInstance().CurrentScenceController;
    }
?
    public int GameJudge()
    {
        int src_priest = sceneController.src_land.GetTotal(0);
        int src_ghost = sceneController.src_land.GetTotal(1);
        int des_priest = sceneController.des_land.GetTotal(0);
        int des_ghost = sceneController.des_land.GetTotal(1);
?
        if (des_priest + des_ghost == 6)
        { //All to the end, win
            return 1;
        }
?
        if (sceneController.boat.GetBoatMark() == 1)//Since the boat has not started yet on this side, you only need to detect the number on the other side.
        {
            if (des_priest < des_ghost & amp; & amp; des_priest > 0)
            {//fail
                return -1;
            }
        }
        else
        {
            if (src_priest < src_ghost & amp; & amp; src_priest > 0)
            {//fail
                return -1;
            }
        }
?
        return 0;//The game continues
    }
}

Model Design

The main classes and interfaces in the code include:

  1. ISceneController interface: defines a method for loading scene resources LoadResources().

  2. IUserAction interface: defines methods related to user operations, including moving the boat (MoveBoat()), restarting the game (Restart()) and Move the role (MoveRole(RoleModel role)).

  3. SSDirector class: Director class, used to manage the current scene controller and implement singleton mode.

  4. RoleModel class: Role model class, representing the role in the game. Each character has a role identifier (role_sign), which can be a priest or a devil, and a status indicating whether he is on a boat (on_boat). The character can move between land and ship, with movement methods (Move(Vector3 end)) and methods for getting on and off the ship (ToLand(LandModel land) and ToBoat (BoatModel boat)).

  5. LandModel class: Land model class, representing the land in the game. Each land object has a mark (land_mark) that is used to distinguish the starting land from the target land. Roles can be placed on land. Roles can be added via the AddRole(RoleModel role) method and removed via the RemoveRole(string name) method. Methods are also provided for obtaining empty positions and counting the number of characters of a specific type.

  6. BoatModel class: Ship model class, representing the ships in the game. Boats have a mark (boat_mark) that distinguishes the landmass on which the boat is located. Two roles can be placed on the ship. Roles can be added via the AddRole(RoleModel role) method and removed via the RemoveRole(string name) method. Methods for obtaining empty positions and counting the number of characters on board are also provided.

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

namespace GhostBoatGame
{
    //Load scene interface
    public interface ISceneController
    {
        void LoadResources();
    }

    //User operation interface
    public interface IUserAction
    {
        void MoveBoat(); //Move the boat
        void Restart(); //Restart
        void MoveRole(RoleModel role); //Move role
    }

    //Director class
    public class SSDirector : System.Object
    {
        private static SSDirector _instance;
        public ISceneController CurrentScenceController { get; set; }

        public static SSDirector GetInstance()
        {
            if (_instance == null)
            {
                _instance = new SSDirector();
            }
            return _instance;
        }
    }

    public class RoleModel
    {
        GameObject role;
        int role_sign; //0 is priest, 1 is devil
        bool on_boat; //whether on the boat
        LandModel land = (SSDirector.GetInstance().CurrentScenceController as Controller).src_land;//The land where it is located

        GhostBoatActionManager moveController;
        Clickable click;//Assign attributes to the model to make it clickable and movable, which is equivalent to adding a script.
        public float speed = 15;

        public RoleModel(int id, Vector3 pos)
        {
            if (id == 0)
            {
                role_sign = 0;
                role = Object.Instantiate(Resources.Load("Prefabs/Priest", typeof(GameObject)), pos, Quaternion.identity) as GameObject;
            }
            else
            {
                role_sign = 1;
                role = Object.Instantiate(Resources.Load("Prefabs/Ghost", typeof(GameObject)), pos, Quaternion.identity) as GameObject;
            }
            click = role.AddComponent(typeof(Clickable)) as Clickable;
            click.SetRole(this);
            moveController = (SSDirector.GetInstance().CurrentScenceController as Controller).action_manager;
        }

        public int GetSign()
        {
            return role_sign;
        }

        public string GetName()
        {
            return role.name;
        }

        public LandModel GetLandModel()
        {
            return land;
        }

        public bool IsOnBoat()
        {
            return on_boat;
        }

        public void SetName(string name)
        {
            role.name = name;
        }

        public void Move(Vector3 end)//External interface for move operation
        {
            Vector3 middle = new Vector3(role.transform.position.x, end.y, end.z);
            moveController.moveRole(role, middle, end, speed);
        }

        public void ToLand(LandModel land)//Move ashore
        {
            Vector3 pos = land.GetEmptyPosition();
            Vector3 middle = new Vector3(role.transform.position.x, pos.y, pos.z);
            moveController.moveRole(role, middle, pos, speed);
            this.land = land;
            on_boat = false;
        }

        public void ToBoat(BoatModel boat)//Movement on board
        {
            Vector3 pos = boat.GetEmptyPosition();
            Vector3 middle = new Vector3(pos.x, role.transform.position.y, pos.z);
            moveController.moveRole(role, middle, pos, speed);
            this.land = null;
            on_boat = true;
        }

        public void Reset()
        {
            LandModel land = (SSDirector.GetInstance().CurrentScenceController as Controller).src_land;
            ToLand(land);
            land.AddRole(this);
        }
    }

    public class LandModel
    {
        GameObject land; //Land object
        public int land_mark;//src is 1, des is -1.
        RoleModel[] roles = new RoleModel[6];//role objects on land
        Vector3[] role_positions;//The position of each role

        public LandModel(int sign)
        {//Initialize according to object identifier
            land_mark = sign;
            land = Object.Instantiate(Resources.Load("Prefabs/Land", typeof(GameObject)), new Vector3(10.5F * land_mark, 0.5F, 0), Quaternion.identity) as GameObject;
            role_positions = new Vector3[] { new Vector3(6.5F * land_mark, 1.8F, 0), new Vector3(8.0F * land_mark, 1.8F, 0), new Vector3(9.5F * land_mark, 1.8F, 0), new Vector3(11.0F * land_mark, 1.8F, 0), new Vector3(12.5F * land_mark, 1.8F, 0), new Vector3(14.0F * land_mark, 1.8F, 0) };
        }

        public int GetLandMark()
        {
            return land_mark;
        }

        public Vector3 GetEmptyPosition()
        {//Find the current empty position
            int pos = -1;
            for (int i = 0; i < 6; i + + )
            {
                if (roles[i] == null)
                {
                    pos = i;
                    break;
                }
            }

            return role_positions[pos];
        }

        public void AddRole(RoleModel role)
        {//Add role
            for (int i = 0; i < 6; i + + )
            {
                if (roles[i] == null)
                {
                    roles[i] = role;
                    break;
                }
            }
        }

        public RoleModel RemoveRole(string name)
        {//Delete role
            for (int i = 0; i < 6; i + + )
            {
                if (roles[i] != null & amp; & amp; roles[i].GetName() == name)
                {
                    roles[i] = null;
                    return roles[i];
                }
            }
            return null;
        }

        public int GetTotal(int id)
        {
            int sum = 0;
            if (id == 0)
            {//Priest
                for (int i = 0; i < 6; i + + )
                {
                    if (roles[i] != null & amp; & amp; roles[i].GetSign() == 0)
                    {
                        sum + + ;
                    }
                }
            }
            else if (id == 1)
            {//devil
                for (int i = 0; i < 6; i + + )
                {
                    if (roles[i] != null & amp; & amp; roles[i].GetSign() == 1)
                    {
                        sum + + ;
                    }
                }
            }
            return sum;
        }

        public void Reset()
        {
            roles = new RoleModel[6];
        }
    }

    public class BoatModel
    {
        GameObject boat;//Boat object
        Vector3[] src_empty_pos;//The ship is in two empty positions on the src land
        Vector3[] des_empty_pos;//The ship is in two empty positions on des land
        public float speed = 15;

        GhostBoatActionManager moveController;
        Clickable click;

        int boat_mark = 1;//The boat is 1 in src and -1 in des.
        RoleModel[] roles = new RoleModel[2];//Two roles on the ship.

        public BoatModel()
        {//Initialize object
            boat = Object.Instantiate(Resources.Load("Prefabs/Boat", typeof(GameObject)), new Vector3(4.5F, 0.5F, 0), Quaternion.identity) as GameObject;
            boat.name = "boat";
            moveController = (SSDirector.GetInstance().CurrentScenceController as Controller).action_manager;
            click = boat.AddComponent(typeof(Clickable)) as Clickable;
            src_empty_pos = new Vector3[] { new Vector3(3.8F, 1.1F, 0), new Vector3(5.2F, 1.1F, 0) };
            des_empty_pos = new Vector3[] { new Vector3(-5.2F, 1.1F, 0), new Vector3(-3.8F, 1.1F, 0) };
        }

        public int Total()
        {
            int sum = 0;
            for (int i = 0; i < 2; i + + )
            {
                if (roles[i] != null)
                {
                    sum + + ;
                }
            }
            return sum;
        }

        public void Move()//While moving the ship, call the character's function Move and move the character to the specified position.
        {
            if (boat_mark == -1)
            {
                moveController.moveBoat(boat, new Vector3(4.5F, 0.5F, 0), speed);
                for (int i = 0; i < 2; i + + )
                {
                    if (roles[i] != null)
                    {
                        roles[i].Move(src_empty_pos[i]);
                    }
                }
                boat_mark = 1;
            }
            else
            {
                moveController.moveBoat(boat, new Vector3(-4.5F, 0.5F, 0), speed);
                for (int i = 0; i < 2; i + + )
                {
                    if (roles[i] != null)
                    {
                        roles[i].Move(des_empty_pos[i]);
                    }
                }
                boat_mark = -1;
            }
        }

        public int GetBoatMark()
        {
            return boat_mark;
        }

        public Vector3 GetEmptyPosition()
        {//Find the current empty position
            if (boat_mark == 1)
            {
                int pos = -1;
                for (int i = 0; i < 2; i + + )
                {
                    if (roles[i] == null)
                    {
                        pos = i;
                        break;
                    }
                }
                return src_empty_pos[pos];
            }
            else
            {
                int pos = -1;
                for (int i = 0; i < 2; i + + )
                {
                    if (roles[i] == null)
                    {
                        pos = i;
                        break;
                    }
                }
                return des_empty_pos[pos];
            }
        }

        public void AddRole(RoleModel role)
        {//Add role
            for (int i = 0; i < 2; i + + )
            {
                if (roles[i] == null)
                {
                    roles[i] = role;
                    break;
                }
            }
        }

        public RoleModel RemoveRole(string name)
        {//Delete role
            for (int i = 0; i < 2; i + + )
            {
                if (roles[i] != null & amp; & amp; roles[i].GetName() == name)
                {
                    roles[i] = null;
                    return roles[i];
                }
            }
            return null;
        }

        public void Reset()
        {
            if (boat_mark == -1)
            {
                moveController.moveBoat(boat, new Vector3(4.5F, 0.5F, 0), speed);
                boat_mark = 1;
            }
            roles = new RoleModel[2];
        }
    }



}

Interface Design

click script

Clickable object scripts for handling click events on characters and ships in the game.

  1. IUserAction action; defines a user operation interface variable action, which is used to call methods in the game controller.
  2. RoleModel role = null; defines a role model variable role, which is used to store the role of the currently clicked object.
  3. public void SetRole(RoleModel role) is a public method used to set the role of the currently clicked object.
  4. private void Start() is a startup method used to obtain the game controller object and assign it to the action variable.
  5. private void OnMouseDown() is a mouse click event method, called when a click occurs. Compare the name of the clicked object to distinguish whether it is a ship or a character.
    • If the name of the clicked object is “boat”, the action.MoveBoat() method is called to move the boat.
    • Otherwise, call the action.MoveRole(role) method to move the role. Role information is stored in the role variable and is set through the SetRole() method
public class Clickable : MonoBehaviour
{
    IUserAction action;
    RoleModel role = null;

    public void SetRole(RoleModel role)
    {
        this.role = role;
    }
    private void Start()
    {
        action = SSDirector.GetInstance().CurrentScenceController as IUserAction;//Get the controller
    }
    private void OnMouseDown()//Call the relevant functions in the controller when clicked, and use the name of the gameObject to distinguish the objects.
    {
        if (gameObject.name == "boat")
        {
            action.MoveBoat();
        }
        else
        {
            action.MoveRole(role);
        }
    }
}

User interface script

Scripts for the user interface that display game status and handle logic for restarting the game.

  1. private IUserAction action; defines a user operation interface variable action, which is used to call methods in the game controller.
  2. public int status = 0; defines an integer variable status, which represents the status of the game. The default value is 0.
  3. bool isShow = false; defines a Boolean variable isShow, which is used to control the display state of the interface. The default is false.
  4. GUIStyle white_style = new GUIStyle(); defines a GUIStyle variable white_style, which is used to set the white font style.
  5. GUIStyle black_style = new GUIStyle(); defines a GUIStyle variable black_style, which is used to set the black font style.
  6. GUIStyle title_style = new GUIStyle(); defines a GUIStyle variable title_style, which is used to set the title font style.

In the Start() method, first obtain the controller object of the current scene and assign it to the action variable. Then, set up different styles of fonts as needed, including white fonts, black fonts, and title fonts.

In the OnGUI() method, the corresponding interface is displayed according to the status of the game. If the game status is -1, which is a failed state, “You Lose!” is displayed in black font and a restart button. When the restart button is clicked, the game controller’s Restart() method is called to restart the game and set status to 0. If the game state is 1, which is the victory state, “You Win!” is displayed in black font and a restart button. When the restart button is clicked, the Restart() method of the game controller is also called to restart the game and the status is set to 0.

public class UserGUI : MonoBehaviour
{
    private IUserAction action;
    public int status = 0;
    bool isShow = false;

    GUIStyle white_style = new GUIStyle();
    GUIStyle black_style = new GUIStyle();
    GUIStyle title_style = new GUIStyle();

    void Start()
    {
        action = SSDirector.GetInstance().CurrentScenceController as IUserAction;

        //Font initialization
        white_style.normal.textColor = Color.white;
        white_style.fontSize = 20;

        black_style.normal.textColor = Color.black;
        black_style.fontSize = 30;

        title_style.normal.textColor = Color.black;
        title_style.fontSize = 45;
    }

    voidOnGUI()
    {
       

        if (status == -1)
        {
            GUI.Label(new Rect(Screen.width / 2 - 60, 180, 100, 30), "You Lose!", black_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 40, 240, 100, 30), "Restart"))
            {
                action.Restart();
                status = 0;
            }
        }
        else if (status == 1)
        {
            GUI.Label(new Rect(Screen.width / 2 - 62, 180, 100, 30), "You Win!", black_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 40, 240, 100, 30), "Restart"))
            {
                action.Restart();
                status = 0;
            }
        }
    }
}