We invite future students of the " Unity Game Developer. Professional " course to watch the open lesson on the topic "Advanced artificial intelligence of enemies in shooters".
And now we are sharing the traditional translation of useful material.
In this tutorial, we will master the Command design pattern and implement it in Unity as part of a game object movement system.
Introducing the Command pattern
Requests, orders and commands : we are all familiar with them in real life; one person sends a request (or order, or command) to another person to perform (or not to perform) some of the tasks assigned to him. In software design and development, this works in a similar way: a request from one component is passed to another to perform specific tasks within the Team pattern.
: β , , () , . , / .
. , ( ). β : (GUI), , (logic handler), -.
GUI , . , GUI -.
UML β. , , .
Command
, (ConcreteCommandN
) Invoker
, Client
Receiver
.
Command
Command (Execute) (Undo) . Execute , , Undo.
public interface ICommand
{
void Execute();
void ExecuteUndo();
}
Invoker
Invoker
( Sender
) . , . , . . , . .
Client
(Client) . , (Receiver), . . , .
Receiver ( )
Receiver () β , -. . , , .
Command . , - ( ). .
, , . (immutable), .
Unity
, Unity . . (Undo), .
, !
3D Unity
3D Unity. CommandDesignPattern
.
Plane, . Hierarchy Plane. Β«GroundΒ» 20 X 20 z. , .
Player
. Capsule
. Hierarchy Capsule
. Player
.
GameManager.cs
Ground
. GameManager.cs
.
Player
.
public GameObject
player
.
public GameObject mPlayer;
Player
Hierarchy
Player
.
(Up, Down, Left Right).
. Update
. 1 .
void Update()
{
Vector3 dir = Vector3.zero;
if (Input.GetKeyDown(KeyCode.UpArrow))
dir.z = 1.0f;
else if (Input.GetKeyDown(KeyCode.DownArrow))
dir.z = -1.0f;
else if (Input.GetKeyDown(KeyCode.LeftArrow))
dir.x = -1.0f;
else if (Input.GetKeyDown(KeyCode.RightArrow))
dir.x = 1.0f;
if (dir != Vector3.zero)
{
_player.transform.position += dir;
}
}
Play
, . (Up, Down, Left Right), .
β Player
Ground
, . ?
, Ground
, .
public Vector3? GetClickPosition()
{
if(Input.GetMouseButtonDown(1))
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(ray, out hitInfo))
{
//Debug.Log("Tag = " + hitInfo.collider.gameObject.tag);
return hitInfo.point;
}
}
return null;
}
Vector3?
?
C#,
public int? myProperty { get; set; }
, nullable
Nullable
,System.Nullable
. ,NULL
,NULL
. ,Nullable<Int32>
,Β«Nullable of Int32Β»
, -2147483648 2147483647,null
.Nullable<bool>
true
,false
null
.null
, , , . ,true
false
, .
, , MoveTo
. MoveTo
. .
public IEnumerator MoveToInSeconds(GameObject objectToMove, Vector3 end, float seconds)
{
float elapsedTime = 0;
Vector3 startingPos = objectToMove.transform.position;
end.y = startingPos.y;
while (elapsedTime < seconds)
{
objectToMove.transform.position = Vector3.Lerp(startingPos, end, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return null;
}
objectToMove.transform.position = end;
}
, , , .
Update
, .
****
var clickPoint = GetClickPosition();
if (clickPoint != null)
{
IEnumerator moveto = MoveToInSeconds(_player, clickPoint.Value, 0.5f);
StartCoroutine(moveto);
}
****
Play
, . (Up, Down, Left Right) Ground
, Player
.
(Undo
)? ? .
Unity
Undo
, , .
Undo
β , Unity.
. Command
.
Command
public interface ICommand { void Execute(); void ExecuteUndo(); }
Command
. β Execute
, β ExecuteUndo
, . ( , ).
.
CommandMove
public class CommandMove : ICommand
{
public CommandMove(GameObject obj, Vector3 direction)
{
mGameObject = obj;
mDirection = direction;
}
public void Execute()
{
mGameObject.transform.position += mDirection;
}
public void ExecuteUndo()
{
mGameObject.transform.position -= mDirection;
}
GameObject mGameObject;
Vector3 mDirection;
}
CommandMoveTo
public class CommandMoveTo : ICommand
{
public CommandMoveTo(GameManager manager, Vector3 startPos, Vector3 destPos)
{
mGameManager = manager;
mDestination = destPos;
mStartPosition = startPos;
}
public void Execute()
{
mGameManager.MoveTo(mDestination);
}
public void ExecuteUndo()
{
mGameManager.MoveTo(mStartPosition);
}
GameManager mGameManager;
Vector3 mDestination;
Vector3 mStartPosition;
}
, ExecuteUndo
. , Execute
.
Invoker
Invoker
. , Invoker
β , . , Undo
Last In First Out (LIFO)
.
LIFO? LIFO? Stack
.
C# , LIFO (Last In First Out). . Push()
( ), Pop()
( ) Peek()
.
Invoker
, .
public class Invoker
{
public Invoker()
{
mCommands = new Stack<ICommand>();
}
public void Execute(ICommand command)
{
if (command != null)
{
mCommands.Push(command);
mCommands.Peek().Execute();
}
}
public void Undo()
{
if(mCommands.Count > 0)
{
mCommands.Peek().ExecuteUndo();
mCommands.Pop();
}
}
Stack<ICommand> mCommands;
}
, Execute
Undo
. Execute
, Push
Execute
. Peek
.
Undo ExecuteUndo
, ( Peek
). Invoker
, Pop
.
Invoker
. Invoker
GameManager
.
private Invoker mInvoker;
mInvoker
Start
GameManager.
mInvoker = new Invoker();
Undo
U
. Update
.
// Undo
if (Input.GetKeyDown(KeyCode.U))
{
mInvoker.Undo();
}
Update
.
void Update()
{
Vector3 dir = Vector3.zero;
if (Input.GetKeyDown(KeyCode.UpArrow))
dir.z = 1.0f;
else if (Input.GetKeyDown(KeyCode.DownArrow))
dir.z = -1.0f;
else if (Input.GetKeyDown(KeyCode.LeftArrow))
dir.x = -1.0f;
else if (Input.GetKeyDown(KeyCode.RightArrow))
dir.x = 1.0f;
if (dir != Vector3.zero)
{
//Using command pattern implementation.
ICommand move = new CommandMove(mPlayer, dir);
mInvoker.Execute(move);
}
var clickPoint = GetClickPosition();
//Using command pattern right click moveto.
if (clickPoint != null)
{
CommandMoveTo moveto = new CommandMoveTo(
this,
mPlayer.transform.position,
clickPoint.Value);
mInvoker.Execute(moveto);
}
// Undo
if (Input.GetKeyDown(KeyCode.U))
{
mInvoker.Undo();
}
}
Play
, . (Up, Down, Left Right), , Β«uΒ» .
β GoF, , - , , , , , .
Unity
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public interface ICommand
{
void Execute();
void ExecuteUndo();
}
public class CommandMove : ICommand
{
public CommandMove(GameObject obj, Vector3 direction)
{
mGameObject = obj;
mDirection = direction;
}
public void Execute()
{
mGameObject.transform.position += mDirection;
}
public void ExecuteUndo()
{
mGameObject.transform.position -= mDirection;
}
GameObject mGameObject;
Vector3 mDirection;
}
public class Invoker
{
public Invoker()
{
mCommands = new Stack<ICommand>();
}
public void Execute(ICommand command)
{
if (command != null)
{
mCommands.Push(command);
mCommands.Peek().Execute();
}
}
public void Undo()
{
if (mCommands.Count > 0)
{
mCommands.Peek().ExecuteUndo();
mCommands.Pop();
}
}
Stack<ICommand> mCommands;
}
public GameObject mPlayer;
private Invoker mInvoker;
public class CommandMoveTo : ICommand
{
public CommandMoveTo(GameManager manager, Vector3 startPos, Vector3 destPos)
{
mGameManager = manager;
mDestination = destPos;
mStartPosition = startPos;
}
public void Execute()
{
mGameManager.MoveTo(mDestination);
}
public void ExecuteUndo()
{
mGameManager.MoveTo(mStartPosition);
}
GameManager mGameManager;
Vector3 mDestination;
Vector3 mStartPosition;
}
public IEnumerator MoveToInSeconds(GameObject objectToMove, Vector3 end, float seconds)
{
float elapsedTime = 0;
Vector3 startingPos = objectToMove.transform.position;
end.y = startingPos.y;
while (elapsedTime < seconds)
{
objectToMove.transform.position = Vector3.Lerp(startingPos, end, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return null;
}
objectToMove.transform.position = end;
}
public Vector3? GetClickPosition()
{
if (Input.GetMouseButtonDown(1))
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hitInfo))
{
//Debug.Log("Tag = " + hitInfo.collider.gameObject.tag);
return hitInfo.point;
}
}
return null;
}
// Start is called before the first frame update
void Start()
{
mInvoker = new Invoker();
}
// Update is called once per frame
void Update()
{
Vector3 dir = Vector3.zero;
if (Input.GetKeyDown(KeyCode.UpArrow))
dir.z = 1.0f;
else if (Input.GetKeyDown(KeyCode.DownArrow))
dir.z = -1.0f;
else if (Input.GetKeyDown(KeyCode.LeftArrow))
dir.x = -1.0f;
else if (Input.GetKeyDown(KeyCode.RightArrow))
dir.x = 1.0f;
if (dir != Vector3.zero)
{
//----------------------------------------------------//
//Using normal implementation.
//mPlayer.transform.position += dir;
//----------------------------------------------------//
//----------------------------------------------------//
//Using command pattern implementation.
ICommand move = new CommandMove(mPlayer, dir);
mInvoker.Execute(move);
//----------------------------------------------------//
}
var clickPoint = GetClickPosition();
//----------------------------------------------------//
//Using normal implementation for right click moveto.
//if (clickPoint != null)
//{
// IEnumerator moveto = MoveToInSeconds(mPlayer, clickPoint.Value, 0.5f);
// StartCoroutine(moveto);
//}
//----------------------------------------------------//
//----------------------------------------------------//
//Using command pattern right click moveto.
if (clickPoint != null)
{
CommandMoveTo moveto = new CommandMoveTo(this, mPlayer.transform.position, clickPoint.Value);
mInvoker.Execute(moveto);
}
//----------------------------------------------------//
//----------------------------------------------------//
// Undo
if (Input.GetKeyDown(KeyCode.U))
{
mInvoker.Undo();
}
//----------------------------------------------------//
}
public void MoveTo(Vector3 pt)
{
IEnumerator moveto = MoveToInSeconds(mPlayer, pt, 0.5f);
StartCoroutine(moveto);
}
}
Wikipedia Command Design Pattern
Design Patterns in Game Programming
"Unity Game Developer. Professional".
" ".