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:
-
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.
-
Scalability: Action separation makes it easier to add new actions. Developers can simply add new action resources without modifying existing game logic and code.
-
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.
-
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:
-
ISceneController
interface: defines a method for loading scene resourcesLoadResources()
. -
IUserAction
interface: defines methods related to user operations, including moving the boat (MoveBoat()
), restarting the game (Restart()
) and Move the role (MoveRole(RoleModel role)
). -
SSDirector
class: Director class, used to manage the current scene controller and implement singleton mode. -
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)
andToBoat (BoatModel boat)
). -
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 theAddRole(RoleModel role)
method and removed via theRemoveRole(string name)
method. Methods are also provided for obtaining empty positions and counting the number of characters of a specific type. -
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 theAddRole(RoleModel role)
method and removed via theRemoveRole(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.
IUserAction action;
defines a user operation interface variableaction
, which is used to call methods in the game controller.RoleModel role = null;
defines a role model variablerole
, which is used to store the role of the currently clicked object.public void SetRole(RoleModel role)
is a public method used to set the role of the currently clicked object.private void Start()
is a startup method used to obtain the game controller object and assign it to theaction
variable.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 therole
variable and is set through theSetRole()
method
- If the name of the clicked object is “boat”, the
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.
private IUserAction action;
defines a user operation interface variableaction
, which is used to call methods in the game controller.public int status = 0;
defines an integer variablestatus
, which represents the status of the game. The default value is 0.bool isShow = false;
defines a Boolean variableisShow
, which is used to control the display state of the interface. The default is false.GUIStyle white_style = new GUIStyle();
defines aGUIStyle
variablewhite_style
, which is used to set the white font style.GUIStyle black_style = new GUIStyle();
defines aGUIStyle
variableblack_style
, which is used to set the black font style.GUIStyle title_style = new GUIStyle();
defines aGUIStyle
variabletitle_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; } } } }