Unity任务系统笔记
概述
任务系统一般基于事件的发布-监听架构。玩家的某些行为发布事件,任务对象监听事件,且需要传递一些参数,不同类型的任务传递的参数不同,不同类型任务参数类的字段包括共用字段和专用字段。
参数一般包括:
- 任务类型;
- 对象Id;
- 数量;
- 更多数据,根据任务类型不同,数据类型也不同,会用一个父类装;
然后有一些设计细节:
不同类任务是发布不同的事件名称还是发布同一个事件,把任务类型作为事件参数的一部分?
任务完成的条件类型
之前射击游戏任务完成的方式有
- 对话
- 拾取任务物品(仅完成任务用,无其他作用)
- 击杀敌人;
- 进入区域
之前发布的事件名是写在对话数据类、任务物品、敌人,都是写在场景里检查器上。且事件都没有参数,完全靠事件名的匹配完成任务。
然后又回到上面的问题,事件分为事件名和事件参数,本质上都是参数,信息可以放事件参数,也可以玩家操作时把操作类型、物品Id、数量动态生成一个事件名。
射击游戏完全靠事件名匹配,因为射击游戏的任务通常靠特殊NPC、特殊物品、任务包含的敌人推进进度。而到了经营、生存类游戏的任务,有一个很大的不同就是很多任务要做的事是后面要重复做很多次的事,不具有特殊性,完全是一般操作。在射击游戏的一关中,一个对话任务完成了,就无法再次触发,也无法再次发布事件,而生存游戏像捡木材、砍树、种植这些操作一直能做,事件是一直在发布的,只是有没有任务监听的问题。这导致不可能只用事件名做匹配,必须事件名+参数,任务收到事件,还要使用参数判定是否满足更新、完成条件。
对于经营、生存类游戏,基本的任务完成条件是操作了Id匹配的对象。
然后任务系统的进阶功能会需要完成条件有一些更复杂的条件
- 《或》条件,比如拾取一个萝卜或玉米的种子;
- 条件不是对象的Id,而是配置里的其他字段,比如拾取一个种子,判断种子的条件是选项列表里有Sow选项;
对于拾起任意类型种子的任务,可以在拾起东西时判断一下是不是种子,是则另外发布一个事件,任务对象监听这个事件。
然而对于指定id范围的任务,脚本里不能硬编码id,
那么必须设计从Excel表、任务参数到判定程序的一系列设计。首先想到把支持的完成条件类型定义枚举,判定方法里switch条件类型,执行
与条件可以拆成多个任务。
一个RPG游戏任务有最基本的:
- 任务名称;
- 详细描述;
- 目的地坐标;
此外,在任务开启、完成时可能触发多种多样的效果:
对NPC的操作:显示、隐藏、移动位置、注入对话(且注入的对话会推进任务进度);
激活敌人或者激活敌人生成器;
任务相关道具的激活;
开启下一个任务;
每种效果要执行不同对象的不同函数,且可能有数量、类型不确定的参数。
然后面临选择:
这些是在场景里配置还是在配表配置?
制作关卡任务的工作流:编辑器vsExcel
制作关卡任务需要精确放置NPC、敌人、任务等,不可能不用编辑器。编辑器和Excel同时用,流程又比较复杂。
最终选择了编辑器制作>导出json。
消除任务对其他对象的引用
任务会触发多种效果,会影响到各种对象,但是如果让任务直接引用对象,引用是无法序列化的。
任务不能持有任何引用,只能有string、int、float、bool等简单数据及其嵌套。
那么一切调用其他对象的效果都必须通过发布事件。
任务类的数据结构设计
基本字段:
string 任务标题;
任务目的地(Vector3或transform,transform更直观);
string 任务详细描述;
基本方法:
开启任务:把这个任务加入任务列表;
完成任务:把这个任务移出任务列表;
触发、完成的条件
任务触发和完成的条件都很多样:对话、战斗、取得物品、到达地点、刺杀等。要触发任务,需要那些可能触发任务的行为能指向任务,这些行为都要加上可能触发的任务字段。触发任务可能是特定行为也可能是一个任务完成。也就是说任务系统不是只加一个系统,还需要对其他系统的行为加回调。
触发、完成的效果
开启和完成任务会有各种效果:某些NPC的出现、消失、移动位置,某些NPC更新可触发的对话、开启下一个任务、生成敌人。
讨论到这里可以发现触发、完成任务很相似,所以能不能把它们做成相同的结构,把可能的效果都包括进去?
任务效果类
我们最终发现,任务效果类就是根据可能触发的事件的参数类型,有多个事件列表,事件包括事件名和事件参数。效果类有一个Trigger方法,把所有列表的每一个事件让事件中心发布一下。各种事件响应者,有监听的事件名字段,把事件和自己的方法绑定。至此我们完成了对象间无直接引用的,能触发多种效果,且扩展很方便的任务系统。
public struct MoveSth{
public string eventName;
public Vector3 pos;
}
public struct FloatEvent
{
public string eventName;
public float val;
}
public class MissionEffects
{
[Header("要触发的Vector3参数事件")]
public List<MoveSth> moveSths;
[Header("要触发事件中心的无参事件")]
public List<string> triggerEvents;
[Header("要触发事件中心的float参数事件")]
public List<FloatEvent> floatEvents;
public void Trigger()
{
if (moveNPCs != null && moveNPCs.Count > 0)
{
MainEventCenter.Single.Trigger<UnityAction>(MainEventCenter.Blackout, () =>
{
foreach (var move in moveNPCs)
{
MissionEventCenter.Single.Trigger(move.eventName, move.pos);
}
});
}
foreach (var eve in triggerEvents)
{
MissionEventCenter.Single.Trigger(eve);
}
foreach(var eve in floatEvents)
{
MissionEventCenter.Single.Trigger(eve.eventName,eve.val);
}
}
}
新的触发效果只需提炼出参数类型,看效果类是否要加新类型事件列表,响应者增加监听事件名字段,和方法绑定。
对对话系统的修改
对话可能完成任务,任务开启后玩家找相关NPC触发对话时完成任务,且把这个任务变量置空,防止再次对话再次完成任务。
对话也可能开启任务,需要在对话结束后开启任务,然后NPC不能再触发这段对话,可能是触发任务进行中的对话,或者没有对话。
和上面触发对话完成任务加起来,对话的开始和结束都要加一个UnityEvent。
消灭名单所有敌人完成的任务
字段:要消灭的敌人列表;
敌人列表的每个敌人也记录自己所属的任务,每个敌人死时,把敌人移出敌人列表,然后判断列表是否为空,若是,则任务完成。
有一个列表记录此关依次要执行的任务对象,关卡流程会清楚很多:

但是多任务分支用列表就不行了。
消灭指定个数敌人完成的任务
敌人有一个字符串列表,记录自己死时分发的事件。然后需要一个监听者,监听到敌人死亡后计数+1,达到需要数量后任务完成。为了减少类数量,就让任务对象来监听。
总结
任务的开启、完成可能被场景里的任意方法触发,开启效果、完成效果可能会执行场景里的任意方法。
两种对话
对话分为两类:能反复触发的对话,对关卡没有推进的对话(简称氛围对话);对关卡有推进,不同阶段触发不同对话的npc(简称推进对话)。
对于推进对话,在关卡不同阶段有不同对话,在一个任务进行中触发同一个对话(如“拜托你了”),实际上出现了完成任务和进行中的多分支。这么多对话的存储位置有几种方案。
1.用List全部记录在NPC对象上。由任务对象指定完成后NPC该说哪一段对话。这样NPC对象脚本的检查器上会存一大堆对话数据。也难以看出一段对话对应哪个任务,是触发任务的对话还是任务进行中的对话。
2.NPC上只记录一段对话数据,就是当前去找ta会触发的对话。任务对象上记录两段对话,任务触发、进行中要说的对话。当关卡进度管理器显示该触发一个任务时,任务对象把触发任务对话写入npc脚本的对话变量,任务触发后把任务进行中对话写入npc对话变量。
这样氛围npc因为没有任务系统修改ta们的对话数据,自然就一直触发同一段对话,不用修改。
很明显2是最优方案,既防止推进进度的npc的脚本里的对话数据过多,又兼容两种npc,而且不推进进度的npc只有一段对话,给ta们声明一个一段对话的List完全是浪费。总之对于氛围NPC,只有一个一段对话变量,对于推进NPC,不同任务阶段对话不同,对话应该记录在任务对象。
问题:回复时开启任务 ,直接把下一个任务NPC要说的话写入了NPC的对话数据,导致当前对话变成下一个任务阶段的
需要在回复时记下要触发任务,这段对话结束后再开启任务。所以在一段对话数据结构里放一个List<UnityEvent>,回复的结构体里有一个UnityEvent用于在检查器配置回调函数,选择该回复时把这个UnityEvent加入列表,对话结束后执行。
问题:如何标记一段对话是氛围的还是推进的?
氛围对话可以反复触发,一般没有回调。推进对话只能触发一次,触发后执行回调。那么有没有回调能作为一段对话是氛围还是推进的标记吗?会遇到一些问题:
- 如果回调是通过unityEvent.AddListener()添加的非永久监听,怎么知道有没有非永久监听?查了一下,要自己记录。
- unityEvent没有添加过监听也不一定是null,无法通过不添加监听然后判断unityEvent==null判定没有回调;
综上,没有准确方法通过UnityEvent判断有没有回调。最终我选择规定不使用AddListener()添加非永久监听,一律使用检查器添加永久监听,使用GetPersistentEventCount()>0判断有回调。这意味着不能用代码写“对话完成任务”的功能,而必须在检查器配置来完成。
然后“对话型任务”这个任务子类也没有必要存在了。
任务和对话的关系
然后我们发现任务触发可以更新对话、任务完成可以更新对话、对话可以触发任务、对话可以完成任务,还有对话的回调在对话开始还是结束时执行的问题。二者可以调用对方,现在是任务更新对话,对话要触发的下一个任务也记在任务里。面临一些选择问题,比如一个任务会给NPC写入一段对话,触发对话会在对话开始或结束时完成这个任务,完成任务的函数是写在任务完成的UnityEvent,还是对话开始或结束的UnityEvent?如果还要开启下一个任务,那么就是有两个函数要填入两个可能的的UnityEvent,选项变多了。
有多个子任务的任务
每完成一个子任务,提示一下,任务界面显示X/Y。比如子任务是移动到某区域,子任务需要触发器,完成后通知父任务,更新完成的子任务数,任务面板提示一下,更新任务列表显示。
父任务要能显示X/Y,要有子任务的列表,检查里面完成的数量和总数。
基于事件中心的设计
想象关卡开始时开启任务1,为一个NPC注入对话,和ta发起对话后完成任务1,对话结束时开启任务2。
任务为NPC注入对话,并且让对话开始时完成任务,结束时开启下一个任务。那么任务有2个字符串列表分别指定对话开始和结束时触发哪些事件。
[Serializable]
public class TalkWithEvents
{
public string talkID;
public List<string> startEvents;
public List<string> endEvents;
}
任务记录自己要监听开启和完成的事件名
if (!string.IsNullOrEmpty(openEffects.listenToEvent))
{
MyEventCenter.Instance.AddListener(openEffects.listenToEvent, Open);
}
if (!string.IsNullOrEmpty(completeEffects.listenToEvent))
{
MyEventCenter.Instance.AddListener(completeEffects.listenToEvent, Complete);
}
销毁前要解除监听
private void OnDestroy()
{
if (!string.IsNullOrEmpty(openEffects.listenToEvent))
{
MyEventCenter.Instance.RemoveListener(openEffects.listenToEvent, Open);
}
if (!string.IsNullOrEmpty(completeEffects.listenToEvent))
{
MyEventCenter.Instance.RemoveListener(completeEffects.listenToEvent, Complete);
}
}
Excel配表设计
任务的有一些成员是和场景分不开的,包括目的地位置、要注入对话的NPC、各种回调。配表里适合记录一些简单类型数据,任务名称、任务描述等。
一个游戏有多个关卡,一个关卡有多个任务,Excel有多个Sheet。我们用一个sheet存一个场景的任务,一个游戏用一个Excel存所有任务。sheet名字标记关卡。
任务开启、完成效果中:赋予对话的NPC、移动NPC的信息、回调都不能拖对象,需要用简单数据类型标记,然后面临选择:每类效果是各用一个字段记录还是所有效果共用一个字段?各类效果的参数数量、类型不同,需要制定一个指令格式,以及解析指令的程序。
为了简单,可以只记录一个事件名字,场景里的管理器维护一个名字-UnityEvent列表。
任务列表
要完成的功能有:
- 按一个键显示当前开启的任务列表,再按一次隐藏;
- 选中一个任务时,该任务选项高亮,显示任务详细描述,并把目的地标记设为显示为该任务的目的地;
- 开启任务时,把该任务加入列表,任务完成时把该任务移除;
- 开启任务时,弹出一个提示;
- 对追踪中任务的管理;
对追踪中任务的管理
- 完成的任务是追踪中任务时,追踪中任务要更新,任务列表有任务则把第一个设置为追踪中任务,没有则设null;
- 接取任务时,如果没有追踪中任务,则把这个设为追踪中任务;
任务对象在hierarchy里的层级
所有任务对象放在一个父对象下面比较清楚。但是一些任务在某目标对象的同一位置,作为目标对象的子对象然后reset位置比较方便。可以给任务加一个public Transform target,Start()时把自己的位置设置到目标处。
问题
协程冲突
在修改NPC位置等突变操作时我写了一个画面渐变为黑色,执行操作,再变透明的函数:
public UnityAction blackoutCallback;
[ContextMenu("画面变黑")]
public void Blackout(){
StartCoroutine(BlackoutCoroutine(blackoutCallback));
}
float fadeSpeed=.04f;
IEnumerator BlackoutCoroutine(UnityAction callback=null){
while(blackBack.color.a<1){
blackBack.color+=new Color(0,0,0,fadeSpeed);
yield return 0;
}
if(callback!=null){
callback.Invoke();
}
while(blackBack.color.a>0){
blackBack.color-=new Color(0,0,0,fadeSpeed);
yield return 0;
}
}
然后这个函数在ContextMenu调用时正常,但是完成任务调用时画面就不变透明了。NPC被正确移动了。然后在第二个while循环里加了个Debug.Log(),发现第二个while循环一直在执行,但是alpha值没有变。

然后又在第一个while循环加了个打印,发现两个while循环都在一直执行。

在协程开头加打印,发现协程被执行了两次。因为一个很笨的错误。

这说明写淡入淡出时如果以imag.color.a作为循环条件,如果在一个淡入淡出完成前开始另一个,两个协程就会打架,淡入淡出永远完不成。实际开发中如果无法避免一个淡入淡出进行中开始另一个,就根据计算好的循环次数,或者直接规定循环次数,并且循环结束后直接把alpha值写成目标值,因为如果不这么做就算循环能结束,最终的alpha会是一个半透明值。
改进后的淡入淡出:
int fadeStep=10;
IEnumerator BlackoutCoroutine(UnityAction callback=null){
for(int i=0;i<fadeStep;i++){
blackBack.color+=new Color(0,0,0,1/(float)fadeStep);
yield return 0;
}
blackBack.color=Color.black;
if(callback!=null){
callback.Invoke();
}
for(int i=0;i<fadeStep;i++){
blackBack.color-=new Color(0,0,0,1/(float)fadeStep);
yield return 0;
}
blackBack.color=new Color(0,0,0,0);
}
对话完后要让屏幕渐变黑,把NPC移到下一个位置并能触发新对话,但是现在对话完后立即就打开交互,能触发下一段对话了
根据设计,对话后这一段对话失效,应该在屏幕变黑、把NPC移到新位置后再让下一个任务把对话赋值给NPC。当前的设计是对话结束后执行回调。
需要改成对话结束后当前对话立即失效,而新对话在屏幕变黑、移动NPC后生效。又不能给对话数据设null使对话失效,因为对话数据里包含对话结束的回调,里面是下一个任务和对话数据。我选择只把对话数据里的句子列表设null,而对话数据里的对话结束回调保留。触发对话时也判断对话数据的句子列表长度是否大于0。
为此,给对话结束回调加一个选项,是直接执行回调还是屏幕变黑执行回调。
稍微复杂的任务完成逻辑
我们会遇到这种情况:
- 玩家需要去3个可疑地点搜索,在任意一个地点找到目标物都算任务完成。玩家可以选择追踪3个地点中的任意一个;
- 同时开启3个任务,玩家需要都完成才开启后面的任务;
基于MVC的任务管理器和面板
数据比较简单,就都放在Controller里了。Controller:不引用View,数据变化时发布事件。
/// <summary>
/// 任务系统控制器
/// </summary>
public class MissionManager : MonoSingleton<MissionManager>{
public const string showMissionList="showMissionList";
public List<MissionBase> initMissions;
public MissionBase chasingMission;
PlayerCharacter player;
Vector3 targetDir;
public List<MissionBase> missionList;
Coroutine coShowMarker;
WaitForSeconds waitUpdateMarker = new WaitForSeconds(.07f);
Rect scale;
Rect canvasRT;
bool showPanel;
//控制器不要去调用面板,让面板监听控制器
public event UnityAction<Vector2,string> onUpdateMarker;
public event UnityAction<MissionBase> onMissionOpen;//需要传入任务名称
public event UnityAction<MissionBase> onMissionDone;
public event UnityAction onChasingChanged;
public event UnityAction<bool> onToggleShowMissions;
protected override void Awake() {
base.Awake();
}
void Start() {
player = MyInput.Single.player;
//画布的rect宽高固定为1920*1080,即使分辨率只是自由16:9
//用于解决分辨率不是1920*1080时任务目的地标记位置错误的问题
canvasRT = (GameSceneManager.Single.canvas.transform as RectTransform).rect;
scale.width = canvasRT.width / Screen.width;
scale.height = canvasRT.height / Screen.height;
coShowMarker = StartCoroutine(ShowMissionMarkIE());
}
IEnumerator ShowMissionMarkIE()
{
while (true)
{
if (chasingMission != null && !chasingMission.hideMark)
{
GetMarkerPos();
}
yield return waitUpdateMarker;
}
}
void OnDisable()
{
if (coShowMarker != null)
{
StopCoroutine(coShowMarker);
}
}
public void MissionComplete(MissionBase mission)
{
if (!missionList.Contains(mission))
{
return;
}
missionList.Remove(mission);
if (chasingMission == mission)//完成的是正在追踪的任务,追踪任务更新
{
if (missionList.Count > 0)
{
SetChasing(0);
}
else
{
SetChasing(-1);
//missionMarker.gameObject.SetActive(false);
}
}
onMissionDone?.Invoke(mission);
}
void GetMarkerPos()
{
Vector3 pos=Camera.main.WorldToScreenPoint(chasingMission.transform.position);
if (pos.z > 0)
{
pos.x = Mathf.Clamp(pos.x,0, Screen.width);
pos.y = Mathf.Clamp(pos.y,0, Screen.height);
}
else {
pos.x = pos.x < Screen.width / 2 ? Screen.width : 0;
pos.y=Screen.height / 2;
}
pos = new Vector2(pos.x * canvasRT.width / Screen.width,
pos.y * canvasRT.height/Screen.height);
string distanceHint = ((int)Vector3.Distance(player.transform.position,
chasingMission.transform.position)).ToString() + "m";
onUpdateMarker?.Invoke(pos, distanceHint);
}
public void SetChasing(int index)
{
if (index >= 0 && index <= missionList.Count)
{
chasingMission = missionList[index];
}
else
{
chasingMission = null;
}
onChasingChanged?.Invoke();
}
public void ToggleMissionList(){
showPanel = !showPanel;
onToggleShowMissions?.Invoke(showPanel);
}
public void HideMissionList(){
if (showPanel)
{
ToggleMissionList();
}
}
public void OpenMission(MissionBase mission){
if (missionList.Contains(mission))
{
return;
}
missionList.Add(mission);
if (!chasingMission)
{
SetChasing(missionList.IndexOf(mission));
}
onMissionOpen?.Invoke(mission);
}
public bool GetMissionPanelOn()
{
return showPanel;
}
/// <summary>
/// 把当前任务列表清空,开启注入的任务
/// </summary>
public void InjectMissions(List<MissionBase>missions)
{
}
}
View:监听Controller的事件。收到输入时调用Controller的
public class PanelMission : PanelBase
{
public const string showMissionList = "showMissionList";
[SerializeField]
RectTransform marker;
[SerializeField]
Text textDistance;
Animator animator;
[SerializeField]
VerticalLayoutGroup missionListRoot;
[SerializeField]
List<Button> missionEntries = new();
[SerializeField]
VerticalLayoutGroup missionHint;
[SerializeField]
Button entryPrefab;
Button buttonOn;
[SerializeField]
Image imageDesc;
[SerializeField]
Text textDesc;
[SerializeField]
Text missionHintPrefab;
const float hintLifeTime = 2.5f;
protected override void Awake()
{
base.Awake();
animator = GetComponent<Animator>();
}
protected override void Init()
{
base.Init();
MissionManager.Single.onUpdateMarker += SetMarkerPos;
MissionManager.Single.onToggleShowMissions += ToggleMissionList;
MissionManager.Single.onMissionOpen += MissionOpen;
MissionManager.Single.onMissionDone += MissionDone;
}
protected override void OnDestroy()
{
base.OnDestroy();
if (MissionManager.Single)
{
MissionManager.Single.onUpdateMarker -= SetMarkerPos;
MissionManager.Single.onToggleShowMissions -= ToggleMissionList;
MissionManager.Single.onMissionOpen -= MissionOpen;
MissionManager.Single.onMissionDone -= MissionDone;
}
}
/// <summary>
/// 控制任务目的地标志的位置
/// </summary>
public void SetMarkerPos(Vector2 pos,string distanceHint)
{
marker.anchoredPosition = pos;
textDistance.text=distanceHint;
}
public void RefreshMissionList()
{
for (int i = 0; i < missionEntries.Count; i++)
{
Destroy(missionEntries[i].gameObject);
}
List<MissionBase> missions = MissionManager.Single.missionList;
for (int i = 0; i < missions.Count; i++)
{
Button mission = Instantiate(entryPrefab, missionListRoot.transform);
Text desc = mission.GetComponentInChildren<Text>();
desc.text = missions[i].missionContent;
Button button = mission.GetComponent<Button>();
int index = i;
button.onClick.AddListener(() => {
imageDesc.gameObject.SetActive(true);
//改变追踪的任务需要V>C>V
MissionManager.Single.SetChasing(index);
//这里改一下,监听控制器
textDesc.text = missions[index].missionDetail;
});
}
}
void UpdateButtonOn(Button button)
{
if (buttonOn == button)
{
return;
}
if (buttonOn)
{
buttonOn.GetComponent<Image>().color = Color.gray;
}
buttonOn = button;
buttonOn.GetComponent<Image>().color = Color.white;
}
/// <summary>
/// 在上方显示一个任务开启、完成的提示。
/// 有多条提示时自动上下排布
/// </summary>
/// <param name="mission"></param>
public void ShowMissionOpen(MissionBase mission)
{
Text hint = Instantiate(missionHintPrefab, missionHint.transform);
hint.text = mission.missionContent;
Destroy(hint.gameObject, hintLifeTime);
}
void ShowMissionDone(MissionBase mission)
{
Text hint = Instantiate(missionHintPrefab, missionHint.transform);
hint.text = mission.missionContent + " 完成";
Destroy(hint.gameObject, hintLifeTime);
}
public void ToggleMissionList(bool show)
{
if (show)
{
ShowMissionList();
}
else
{
HideMissionList();
}
}
/// <summary>
/// 开启的任务列表显示出来
/// </summary>
public void ShowMissionList()
{
animator.SetBool(showMissionList, true);
Cursor.lockState = CursorLockMode.None;
MyInput.Single.playerInput.SwitchCurrentActionMap(MyInput.actionMapMissionPanel);
//int i = missionList.IndexOf(chasingMission);
//if (i >= 0)
//{
// Button button = missionListRoot.transform.GetChild(i).GetComponent<Button>();
// UpdateButtonOn(button);
//}
}
public void HideMissionList()
{
animator.SetBool(showMissionList, false);
Cursor.lockState = CursorLockMode.Locked;
MyInput.Single.playerInput.SwitchCurrentActionMap(MyInput.actionMapPlayer);
imageDesc.gameObject.SetActive(false);
}
/// <summary>
/// 有任务开启时,做两件事:
/// 刷新任务列表,上方弹出提示
/// </summary>
void MissionOpen(MissionBase mission)
{
RefreshMissionList();
ShowMissionOpen(mission);
}
void MissionDone(MissionBase mission)
{
RefreshMissionList();
ShowMissionDone(mission);
}
}
写一个任务系统用的事件中心
特点是事件名不写死在脚本,而是来自配置或场景。我们要先定义任务会触发的所有效果种类,
- 给npc注入对话
- 移动npc位置
- 激活敌人生成器
- 改变光源角度
- 触发随机放置敌人
我们知道不同效果需要不同参数,在这之前,要触发的对象+效果种类本身就是参数,这两个参数我们可以都放到事件名来表示。
这里事件名也是变量,也可以当参数。事件名可以直接用来确定要影响的对象+函数。比如激活敌人生成器,写一个事件名,生成器那边写一样的事件名,用激活方法监听。
像移动物体这种,必须传一个vector3,那么任务发事件,对面,不管是npc、道具还是什么,就在检查器开放一个事件名,监听这个事件,移动到参数给的位置。无需再给npc、道具加id。
更多推荐



所有评论(0)