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.