Unity-小游戏 牧师和恶魔
游戏介绍
当游戏开始时,三个牧师和三个恶魔都在河的一侧。游戏的目标是帮助他们渡河到达对岸,并且在任何一侧,牧师的数量不能少于恶魔的数量,否则恶魔会袭击牧师。游戏的规则如下:
角色和道具:
- 牧师(P): 游戏中有三个牧师,用P表示。
- 恶魔(D): 游戏中有三个恶魔,用D表示。
- 船(B): 游戏中有一艘船,用B表示。船的最大容量为两人。
初始状态:
- 初始时,三个牧师(PPP)和三个恶魔(DDD)以及船(B)都在河的一侧。
目标状态:
- 游戏的目标是将所有的牧师和恶魔都安全地带到河的另一侧,且在任何一侧恶魔的数量都不能多于牧师的数量(例如,在河的任意一侧,PD或PDD是合法状态,但DD是非法状态)。
移动规则:
- 船每次最多可以携带两个角色(牧师、恶魔或者两者混合)过河。
- 在任何一侧,恶魔的数量不能多于牧师的数量,否则恶魔会袭击牧师,游戏失败。
- 牧师和恶魔都可以操作船,但船上的角色数量不能超过两个。
游戏结束:
游戏结束有两种情况:
- 当所有的牧师和恶魔都安全地渡到了河的对岸,且符合条件(恶魔数量不多于牧师数量)时,游戏胜利。
- 如果在任何一侧,恶魔数量多于牧师数量,恶魔就会袭击牧师,游戏失败。
"牧师与恶魔"是一种经典的智力游戏,需要玩家根据规则巧妙地安排角色的位置和船的乘客,以确保所有的角色都能安全渡河,是一个考验逻辑思维和规划能力的游戏。
设计架构(基于动作分离)
MVC框架介绍
利用MVC(Model-View-Controller)思想,游戏结构更加清晰。
1. 类介绍
-
导演(Director)类:使用单实例模式实现,负责游戏全局设定、场景管理等工作。
-
场景经理(SceneManager)类:实现游戏模型管理与游戏逻辑的实现,符合MVC框架,将用户视图与游戏场景模型的逻辑分离。
-
人机交互(IUserAction)接口:描述游戏规则对应操作,定义了游戏中的用户交互行为。
2. MVC框架工作流程
-
导演对象设置:导演类负责游戏的全局设定,包括初始化游戏场景、角色等信息。
-
场景切换:场景经理负责管理游戏场景,包括切换不同的游戏场景,确保游戏流程的顺利进行。
-
用户操作处理:用户的操作由实现了
IUserAction
接口的类处理,该接口定义了游戏规则对应的操作,例如玩家移动、攻击等。
3. 优点
-
结构清晰:MVC框架使得游戏结构更加清晰,分离了数据模型、用户视图和控制逻辑。
-
模块化开发:不同的模块(Model、View、Controller)可以独立开发和测试,提高了代码的可维护性和可扩展性。
-
逻辑分离:通过MVC,实现了用户视图与游戏场景模型的逻辑分离,降低了耦合性。
-
统一的场景控制:建立了统一的场景控制接口,支持不同场景不同的业务逻辑,增加了灵活性。
具体设计方案如下所示:
动作分离设计的想法
随着业务或者游戏的规则不断增加,动作组合也随之变复杂,场景控制器不仅要处理用户交互事件,还要游戏对象加载、游戏规则实现、运动实现等等,而显得非常臃肿。
由此可见,假如我们还是使用上面的MVC框架进行游戏的开发和维护,无疑会增加维护和开发的成本,而一个更好的方案就是,基于面向对象编程的思想,我们在其中抽取出如动作等相关性元素,分出接口和类来进行单独处理,这样不仅可以让程序更能适应需求变化,对象更容易被复用,程序更易于维护。
结合具体的游戏逻辑,我们就需要设计实现一个动作管理器来统一管理动作,并且需要把判断规则的判断抽离出来一个裁判类实现。
动作管理器的设计思路
-
门面模式(控制器模式)输出组合好的动作:
- 通过门面模式输出组合好的几个动作,供原程序调用。
- 好处是,动作的组合成为动作模块内部的事务。这个门面称为
CCActionManager
。
-
组合模式实现动作组合:
- 使用组合模式设计方法,确保有一个抽象事物表示该类事物的共性,例如
SSAction
,表示动作,无论是基本动作还是组合后的动作。 - 基本动作由用户设计,例如
CCMoveToAction
。 - 组合动作由基本或组合动作组合而成,例如
CCSequenceAction
。
- 使用组合模式设计方法,确保有一个抽象事物表示该类事物的共性,例如
-
接口回调(函数回调)实现解耦:
- 组合对象实现一个事件抽象接口
ISSCallback
,作为监听器(listener)监听子动作的事件。 - 被组合对象使用监听器传递消息给管理者,而管理者的具体处理由实现这个监听器的人来决定。
- 组合对象实现一个事件抽象接口
-
模板方法减少细节要求:
- 使用模板方法,让使用者不需要关心动作管理过程的细节。
SSActionManager
作为CCActionManager
的基类,提供基本的模板方法。
这种设计思路可以确保动作的组合与实现细节被封装起来,使得整个系统更加灵活、易于扩展和维护。
具体代码实现
SSActionCallBack
/**
* 定义一个接口用于动作完成后的回调操作。
*/
public interface SSActionCallback {
/**
* 当动作完成时调用的方法。
*/
void actionDone(SSAction source);
}
接口SSActionCallBack
定义一个动作完成后的回调操作。接口中只包含了一个抽象方法 actionDone,该方法接收一个参数 source,表示发起动作的对象。
SSAction
// SSAction 类继承自 ScriptableObject,表示游戏对象的动作行为。
public class SSAction : ScriptableObject
{
// 是否启用该动作,默认为 true。
public bool enable = true;
// 是否在动作完成后销毁游戏对象,默认为 false。
public bool destroy = false;
// 游戏对象的引用。
public GameObject gameObject;
// 游戏对象的变换组件引用。
public Transform transform;
// 动作完成后通知的回调接口。
public SSActionCallback whoToNotify;
// 虚拟方法,表示动作开始时的操作,需要在子类中进行实现。
public virtual void Start()
{
// 抛出未实现异常,子类需要覆盖该方法。
throw new System.NotImplementedException();
}
// 虚拟方法,表示动作更新时的操作,需要在子类中进行实现。
public virtual void Update()
{
// 抛出未实现异常,子类需要覆盖该方法。
throw new System.NotImplementedException();
}
}
接口SSAction
解释:
SSAction
类是一个继承自ScriptableObject
的类,用于表示游戏对象的动作行为。enable
表示该动作是否启用,默认为true
。destroy
表示动作完成后是否销毁游戏对象,默认为false
。gameObject
是该动作关联的游戏对象的引用。transform
是游戏对象的变换组件引用。whoToNotify
是一个动作完成后通知的回调接口,类型为ActionCallback
。Start()
是一个虚拟方法,表示动作开始时的操作。在基类中,它抛出了未实现异常,需要在子类中进行实现。Update()
是一个虚拟方法,表示动作更新时的操作。同样地,它也抛出了未实现异常,需要在子类中进行实现。
该类提供了动作的基本结构,并且支持回调机制,当动作完成后,可以通过 whoToNotify
属性指定的回调接口通知其他部分的代码。
SSActionManage
// SSActionManage 类,用于管理游戏中的动作,实现了 SSActionCallback 接口。
public class SSActionManage : MonoBehaviour, SSActionCallback {
// 存储动作的字典,键为动作的实例ID,值为 SSAction 对象。
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
// 待添加的动作列表。
private List<SSAction> waitingToAdd = new List<SSAction>();
// 待删除的动作实例ID列表。
private List<int> waitingToDelete = new List<int>();
// 每帧调用一次,用于更新动作的状态。
protected void Update() {
// 将待添加的动作加入到字典中。
foreach (SSAction ac in waitingToAdd) {
actions[ac.GetInstanceID()] = ac;
}
waitingToAdd.Clear();
// 遍历字典中的动作,更新动作状态,如果动作被标记为销毁,则加入待删除列表。
foreach (KeyValuePair<int, SSAction> kv in actions) {
SSAction ac = kv.Value;
if (ac.destroy) {
waitingToDelete.Add(ac.GetInstanceID());
} else if (ac.enable) {
ac.Update();
}
}
// 遍历待删除列表,从字典中移除相应的动作,然后销毁动作对象。
foreach (int key in waitingToDelete) {
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
waitingToDelete.Clear();
}
// 添加动作到管理器中的方法,初始化动作的属性,并将其加入待添加列表,然后调用动作的 Start 方法。
public void addAction(GameObject gameObject, SSAction action, SSActionCallback whoToNotify) {
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.whoToNotify = whoToNotify;
waitingToAdd.Add(action);
action.Start();
}
// 实现 SSActionCallback 接口的方法,当动作完成时被调用。
public void actionDone(SSAction source) {
// 在这里可以添加动作完成后的逻辑处理。
}
}
代码解析:
-
SSActionManage
类是用于管理游戏中动作的类,它继承了MonoBehaviour
类,并实现了SSActionCallback
接口。 -
actions
字典用于存储游戏中的动作,键为动作的实例ID,值为SSAction
对象。 -
waitingToAdd
列表用于存储待添加的动作。 -
waitingToDelete
列表用于存储待删除的动作的实例ID。 -
Update()
方法在每一帧中被调用,用于更新动作的状态。它遍历待添加的动作列表,将动作加入到字典中;然后遍历字典中的动作,更新动作状态,如果动作被标记为销毁,则加入待删除列表;最后遍历待删除列表,从字典中移除相应的动作,然后销毁动作对象。 -
addAction()
方法用于将动作添加到管理器中。它初始化动作的属性,将动作加入待添加列表,并调用动作的Start()
方法。 -
actionDone()
方法是实现了SSActionCallback
接口的方法,当动作完成时被调用。
SSMoveToAction
// SSMoveToAction 类,继承自 SSAction,用于实现游戏对象的移动动作。
public class SSMoveToAction : SSAction {
// 移动的目标位置。
public Vector3 target;
// 移动的速度。
public float speed;
// 私有的构造函数,确保该类不能被外部直接实例化。
private SSMoveToAction() {
}
// 静态方法,用于获取 SSMoveToAction 的实例。
public static SSMoveToAction getAction(Vector3 target, float speed) {
SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();
action.target = target;
action.speed = speed;
return action;
}
// 重写基类的 Update 方法,实现移动逻辑。
public override void Update() {
// 使用 MoveTowards 方法将当前位置移向目标位置。
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
// 如果当前位置达到目标位置,则标记为销毁,并调用回调接口的 actionDone 方法。
if (this.transform.position == target) {
this.destroy = true;
this.whoToNotify.actionDone(this);
}
}
// 重写基类的 Start 方法。
public override void Start() {
}
}
代码解析:
-
SSMoveToAction
类继承自SSAction
,用于实现游戏对象的移动动作。 -
target
属性表示移动的目标位置,speed
属性表示移动的速度。 -
私有的构造函数确保该类不能被外部直接实例化,而是通过静态方法
getAction(Vector3 target, float speed)
来获取SSMoveToAction
的实例。 -
Update()
方法是重写基类的方法,在每一帧中被调用,实现了移动逻辑。使用Vector3.MoveTowards
方法将当前位置移向目标位置,移动的速度受speed
和Time.deltaTime
的影响。如果当前位置达到目标位置,则将destroy
标记为true
,并通过回调接口的actionDone
方法通知动作已完成。 -
Start()
方法是重写基类的方法。
该类实现了游戏对象的移动动作,通过 Update()
方法在每一帧中更新游戏对象的位置,当对象移动到目标位置时,标记为销毁,并通知回调接口。
SequenceAction
代码解释和文字分析:
// SequenceAction 类,继承自 SSAction,实现了 SSActionCallback 接口,用于按顺序执行一系列动作。
public class SequenceAction : SSAction, SSActionCallback {
// 保存要执行的一系列动作。
public List<SSAction> sequence;
// 动作重复执行的次数,1->只执行一次,-1->无限重复。
public int repeat = 1;
// 当前执行的动作在序列中的索引。
public int currentActionIndex = 0;
// 静态方法,用于获取 SequenceAction 的实例。
public static SequenceAction getAction(int repeat, int currentActionIndex, List<SSAction> sequence) {
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
action.sequence = sequence;
action.repeat = repeat;
action.currentActionIndex = currentActionIndex;
return action;
}
// 重写基类的 Update 方法,用于更新动作的状态。
public override void Update() {
// 如果序列中没有动作,则直接返回。
if (sequence.Count == 0) return;
// 如果当前执行的动作索引在序列范围内,更新当前动作的状态。
if (currentActionIndex < sequence.Count) {
sequence[currentActionIndex].Update();
}
}
// 实现 SSActionCallback 接口的方法,在动作完成时被调用。
public void actionDone(SSAction source) {
// 标记当前动作为未销毁状态,切换到下一个动作。
source.destroy = false;
this.currentActionIndex++;
// 如果当前动作索引超过了序列的长度。
if (this.currentActionIndex >= sequence.Count) {
this.currentActionIndex = 0;
// 如果动作需要重复执行,减少重复次数。
if (repeat > 0) repeat--;
// 如果重复次数为0,标记该动作为销毁状态,并通知回调接口动作完成。
if (repeat == 0) {
this.destroy = true;
this.whoToNotify.actionDone(this);
}
}
}
// 重写基类的 Start 方法,用于初始化动作。
public override void Start() {
// 将每个动作关联到当前游戏对象,设置回调接口,并调用动作的 Start 方法。
foreach (SSAction action in sequence) {
action.gameObject = this.gameObject;
action.transform = this.transform;
action.whoToNotify = this;
action.Start();
}
}
// 当对象被销毁时,销毁序列中的每个动作。
void OnDestroy() {
foreach (SSAction action in sequence) {
DestroyObject(action);
}
}
}
代码解析:
-
SequenceAction
类继承自SSAction
类,实现了按顺序执行一系列动作的逻辑。同时,它实现了SSActionCallback
接口,用于在动作完成时进行回调操作。 -
sequence
属性保存了要按顺序执行的一系列动作。 -
repeat
属性表示动作的重复执行次数,1 表示只执行一次,-1 表示无限重复。 -
currentActionIndex
属性表示当前执行的动作在序列中的索引。 -
getAction
是一个静态方法,用于获取SequenceAction
的实例,并初始化其属性。 -
Update
方法重写了基类的方法,用于更新动作的状态。它检查当前动作索引是否在序列的范围内,如果是,则更新当前动作的状态。 -
actionDone
方法实现了SSActionCallback
接口的方法,在动作完成时被调用。它标记当前动作为未销毁状态,切换到下一个动作,并根据重复次数的设定来处理动作的重复执行和销毁通知。 -
Start
方法重写了基类的方法,用于初始化动作。它将每个动作关联到当前游戏对象,设置回调接口,并调用动作的Start
方法。 -
OnDestroy
方法是一个 Unity 生命周期方法,当对象被销毁时调用,用于销毁序列中的每个动作对象。
SceneActionMange
// SceneActionManager 类,继承自 SSActionManager,负责管理游戏场景中的动作。
public class SceneActionManager : SSActionManager {
// 移动船到目标位置的方法。
public void moveBoat(BoatController boat) {
// 创建一个移动动作,将船移动到目标位置。
SSMoveToAction action = SSMoveToAction.getAction(boat.getDestination(), boat.movingSpeed);
// 将动作添加到动作管理器中,设置回调为当前实例。
this.addAction(boat.getGameobj(), action, this);
}
// 移动角色到目标位置的方法。
public void moveCharacter(MyCharacterController characterCtrl, Vector3 destination) {
// 获取当前角色的位置。
Vector3 currentPos = characterCtrl.getPos();
Vector3 middlePos = currentPos;
// 根据目标位置和当前位置的关系,确定中间位置,确保角色移动路径合理。
if (destination.y > currentPos.y) {
// 从低处(船)到高处(岸)
middlePos.y = destination.y;
} else {
// 从高处(岸)到低处(船)
middlePos.x = destination.x;
}
// 创建两个移动动作,分别将角色移动到中间位置和目标位置。
SSAction action1 = SSMoveToAction.getAction(middlePos, characterCtrl.movingSpeed);
SSAction action2 = SSMoveToAction.getAction(destination, characterCtrl.movingSpeed);
// 创建一个顺序动作,依次执行两个移动动作。
SSAction seqAction = SequenceAction.getAction(1, 0, new List<SSAction> {
action1, action2 });
// 将顺序动作添加到动作管理器中,设置回调为当前实例。
this.addAction(characterCtrl.getGameobj(), seqAction, this);
}
}
代码解析:
-
SceneActionManager
类继承自SSActionManager
类,用于管理游戏场景中的动作。 -
moveBoat
方法负责将船移动到目标位置。它创建一个移动动作SSMoveToAction
,将船的目标位置和移动速度作为参数传递给该动作。然后将该动作添加到动作管理器中,回调设置为当前实例。 -
moveCharacter
方法负责将角色移动到目标位置。它先根据目标位置和当前位置的关系,确定一个中间位置,确保角色移动路径合理。然后创建两个移动动作SSMoveToAction
,分别将角色移动到中间位置和目标位置。接着,创建一个顺序动作SequenceAction
,将这两个移动动作作为子动作传递给顺序动作。最后,将顺序动作添加到动作管理器中,回调设置为当前实例。 -
这些方法的目的是将游戏场景中的角色和船按照指定的路径移动到目标位置,实现游戏中的角色与船的交互动作。
开发总结
在开发这个“牧师与恶魔”游戏的过程中,我借鉴了很多网上的参考博客和代码学到了许多关于游戏开发的重要概念和技能。以下是开发总结的一些关键点:
1. 游戏设计与规划
- 游戏规则定义: 游戏规则需要明确定义,包括角色能力、游戏胜利条件和失败条件等。
- 角色设计: 游戏中的角色应该具有明确的身份和特性,使得游戏具有足够的深度和策略性。
2. 编程技能
- 接口与实现: 使用接口将游戏逻辑和用户操作分离,提高了代码的模块化和可维护性。
- 单例模式: 使用单例模式来管理游戏状态,确保在整个游戏生命周期内只有一个实例。
- GUI 编程: 学会使用 Unity 的 GUI 系统来创建游戏界面,包括按钮、标签等 UI 元素。
3. 逻辑思维与问题解决
- 游戏状态判断: 编写游戏逻辑,判断游戏是否胜利或失败,需要逻辑思维和问题解决能力。
- 游戏机制设计: 设计游戏规则和角色能力,需要深入思考游戏的平衡性和趣味性。
4. 用户体验与界面设计
- 用户界面设计: 设计用户友好的游戏界面,包括按钮的位置、字体的大小等,以提供良好的用户体验。
- 游戏反馈: 在游戏中提供适当的反馈,例如游戏胜利或失败时显示相应的提示信息,增加游戏的互动性。
视频展示
unity小游戏 牧师与恶魔
参考博客
http://t.csdnimg.cn/UbqPT
http://t.csdnimg.cn/T5rqW