在学习银鸟工作室SC-102时的笔记
[视频链接](https://www.bilibili.com/video/BV1vL411i7qv/?spm_id_from=333.788&vd_source=c207d130414b95c492ab89714eede578)
所谓游戏的设计模式,就是我们在实现游戏功能时可以遵循的模板或者思想,它们通常是前人在遇到经常出现的问题时总结出来的解决方案。
常见的设计模式有:
- 单例模式
- 指令模式
- 工厂模式
- 观察者模式
- 策略模式
一、单例模式
单例模式是一个最简单、最常用、但也最容易被滥用的设计模式
单例模式有两个特点:
- 始终只允许有一个该类的实例
- 对于该类的实例有一个全局的访问点
一个简单的单例类模板为:
public class designPattern : MonoBehaviour
{
void Start()
{
Singleton.Instance.show();
}
}
public class Singleton : MonoBehaviour
{
private static Singleton instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if(instance == null)
{
instance = new Singleton();
}
return instance;
}
}
public void show()
{
print("Singleton");
}
}
单例类最核心的一行代码是
private Singleton() { }
将构造函数设为private,使得这个类在类外不能够被实例化,实现了对该类实例数量的控制,使其无法在类外实例化:
此外,单例类的唯一实例,以及访问这个实例的方法都需要是static的,可以保证该实例全局有效,而且可以通过类名访问实现全局访问。
当我们需要很多个单例类时,可以创建一个通用的单例类,其他单例类继承自它就好了。
public class Singleton<T>:MonoBehaviour where T:class,new()
{
protected Singleton() { }
private static T instance = null;
public static T Instance => instance ??= new T();
public static void Clear()
{
instance = null;
}
}
使用这个通用单例可以这样使用:
public class GameManage : Singleton<GameManage>
{
public void Show()
{
print("show");
}
}
需要注意的是,GameManage类是可以有多个实例的,但是所对应的instance只有一个,比如调用Show函数可以有两种方法:
GameManage gameManage = new();
gameManage.Show();
GameManage.Instance.Show();
只有Instance和gameManage都是GameManage的实例,但是只有Instance是全局唯一且有效的,而新创建的gameManage中没有全局的数据。
也就是说,只有Instance是唯一有效的全局单例。
二、指令模式
指令模式是将每一个操作抽象成一个指令,每一个指令都有一个执行操作和撤销操作。
指令模式通常用来实现操作撤回的效果,将执行过的每一条指令存储起来,撤回时使用调用撤回操作即可。
1、指令模式下,每一条指令都有一个执行操作和撤销操作,可以设置一个抽象类作为父类,包含一个执行操作和一个撤回操作:
public abstract class Command
{
public abstract void Execute(GameObject gameObject, float input);
public abstract void Undo(GameObject gameObject, float input);
}
每一个实际的指令都是Command的子类,并实现具体的执行和撤回操作
public class MoveForwordCommand : Command
{
public override void Execute(GameObject gameObject, float input)
{
gameObject.transform.Translate(Vector3.forward * input);
}
public override void Undo(GameObject gameObject, float input)
{
gameObject.transform.Translate(-Vector3.forward * input);
}
}
public class MoveBackCommand : Command
{
public override void Execute(GameObject gameObject, float input)
{
gameObject.transform.Translate(Vector3.back * input);
}
public override void Undo(GameObject gameObject, float input)
{
gameObject.transform.Translate(-Vector3.back * input);
}
}
public class MoveRightCommand : Command
{
public override void Execute(GameObject gameObject, float input)
{
gameObject.transform.Translate(Vector3.right * input);
}
public override void Undo(GameObject gameObject, float input)
{
gameObject.transform.Translate(-Vector3.right * input);
}
}
public class MoveLeftCommand : Command
{
public override void Execute(GameObject gameObject, float input)
{
gameObject.transform.Translate(Vector3.left * input);
}
public override void Undo(GameObject gameObject, float input)
{
gameObject.transform.Translate(-Vector3.left * input);
}
}
2、所有命令在声明是同一使用父类声明,在实现时使用对应的子类实现,便于管理。
//声明
private Command moveForwordCommand;
private Command moveBackCommand;
private Command moveLeftCommand;
private Command moveRightCommand;
//实现
private void Awake()
{
moveForwordCommand = new MoveForwordCommand();
moveBackCommand = new MoveBackCommand();
moveLeftCommand = new MoveLeftCommand();
moveRightCommand = new MoveRightCommand();
}
3、每次指令一个命令时,我们需要将命令保存起来,这里可以使用栈来存储已经执行过的命令。
每次执行完一条命令,就将这条命令压栈。
Stack<Command> commands = new();
private void Update()
{
if(Input.GetKeyDown(KeyCode.W))
{
moveForwordCommand.Execute(cube, moveSpeed);
commands.Push(moveForwordCommand);
}
if (Input.GetKeyDown(KeyCode.S))
{
moveBackCommand.Execute(cube, moveSpeed);
commands.Push(moveBackCommand);
}
if(Input.GetKeyDown(KeyCode.D))
{
moveRightCommand.Execute(cube,moveSpeed);
commands.Push(moveRightCommand);
}
if( Input.GetKeyDown(KeyCode.A))
{
moveLeftCommand.Execute(cube,moveSpeed);
commands.Push(moveLeftCommand);
}
}
4、当执行撤回操作时,将栈顶元素出栈,然后执行该元素的撤回操作。这里就体现了抽象子类的好处。由于我们声明时统一声明的是父类,存储的也是父类,在执行撤回操作时,不需要知道具体是什么操作,只需要直接调用相同的Undo函数即可。
private void Update()
{
if(Input.GetKey(KeyCode.LeftControl))
{
if (Input.GetKeyDown(KeyCode.Z))
{
if(commands.Count > 0)
{
Command command = commands.Pop();
command.Undo(cube, moveSpeed);
}
}
}
}
- 可以留意下Ctrl+Z的实现方法
- 可以看到,在执行Updo函数时还提供了参数,其实最好将参数也封装在子类中,使Updo操作没有参数也没有返回值,这样不容易出错
三、工厂模式
工厂模式就是一个用于生产物体的工厂,它将类的实例化过程包装起来,使得使用者可以方便且可控的获得一个类的实例。
比如:
public abstract class Animal
{
public abstract string Speak();
}
public class Dog : Animal
{
public override string Speak()
{
return "Woof";
}
}
public class Cat : Animal
{
public override string Speak()
{
return "Meow";
}
}
看起来和指令模式有点像,其实就是在我们创建敌人、物体等实例时,可以方便我们创建这些实例,而且可以对实例进行同一的管理。
比如:
public static class AnimalFactory
{
public static Animal CreatAnimals(string type)
{
switch(type)
{
case "Dog":
return new Dog();
case "Cat":
return new Cat();
default:
throw new NotSupportedException();
}
}
}
上面时一个简单的工厂,我们可以在创建不同的实例时,进行不同的加工,然后再返回实例。一个实例工厂可以通过不同的静态方法,返回不同的父类实例对象。
此外,也可以将工厂本身进行抽象化:
public interface IAnimalFactory
{
Animal CreatAnimals(string type);
}
public class AnimalFactory : IAnimalFactory
{
public Animal CreatAnimals(string type)
{
switch(type)
{
case "Dog":
return new Dog();
case "Cat":
return new Cat();
default:
throw new NotSupportedException();
}
}
}
这样就可以实现多个不同的工厂,每个工厂之间具体的实现方法可以有差别
四、观察者模式
观察者模式存在观察者和通知者两方。当通知者发生变化时,会通知所有观察者做出反应。
Unity中的委托就是观察者模式的实现,Action和Func都是观察者模式,Action、Func函数本身是通知者,其中包含的函数是观察者。当调用Action和Func时,会通知其包含的所有函数执行操作。
此外Button也是一个经典的观察者模式
OnClick()函数就是通知者,包含的所有函数是观察者,当按钮按下时,会通知其中添加的所有函数进行执行操作。
五、策略模式
策略模式就是指再运行程序时,根据具体情况选择一种算法来执行任务,是一种很常见也很常用的思路
非常简单的例子:
void TakeDamage()
{
if (InWater())
{
TakeDamageInWater();
}
else
{
TakeDamageInLand();
}
}
private void TakeDamageInLand() //在陆地收到的伤害计算
{
throw new NotImplementedException();
}
private void TakeDamageInWater() //在水中收到的伤害计算
{
throw new NotImplementedException();
}
private bool InWater() //是否在水中判断逻辑
{
throw new NotImplementedException();
}