第十一章 地图动作与地图事件(Map Action and Map Event)
我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制。
我们这一章来完成地图上的操作,地图的操作将全部由MapAction
控制。
七 地图事件的结果(Result of Map Event)
地图事件是我们地图中所发生的事情,包括:
-
触发条件;
-
触发结果。
它也比较常见,例如:
条件 | 结果 | FE4中实例 |
---|---|---|
地图开始 | 创建各种地图对象 | 战斗开始时的剧情,然后创建地图对象 |
角色到达某个地点 | 获取物品 | 第一章时斧骑士到达海边获取二回攻击斧 |
某回合开始 | 创建各种地图对象 | 某些剧情需要 |
对应角色对话 | 获取物品、提升属性等 | 某些章节对话 |
等等诸如此类的事件。
这一节,我们挑选一些典型的事件来说明。
1 结果类(Class Result)
和条件类似的,每个事件触发的结果可能是多种。
首先,我们先来归类一下结果类型:
创建一个枚举MapEventResultType
来表示结果类型(它们不是必须的,按需来):
using System;
namespace DR.Book.SRPG_Dev.ScriptManagement
{
[Serializable]
public enum MapEventResultType
{
NoneResult = 0,
/// <summary>
/// 剧本
/// </summary>
ScenarioResult,
/// <summary>
/// 创建MapObject
/// </summary>
CreateObjectResult,
/// <summary>
/// 传送位置
/// </summary>
PositionResult,
/// <summary>
/// 增加/减少属性
/// </summary>
PropertyResult,
/// <summary>
/// 获得/遗失物品
/// </summary>
ItemResult,
/// <summary>
/// 战斗胜利
/// </summary>
WinResult,
/// <summary>
/// 战斗失败
/// </summary>
LoseResult
}
}
这些结果,我们规定:
-
NoneResult
:没有任何结果; -
ScenarioResult
:剧本结果,触发剧情; -
CreateObjectResult
:创建对象结果,创建地图障碍物或角色等(你也可以添加一个变量,判断是创建还是消除,我在这里只是创建); -
PositionResult
:位置结果,它用来表示将角色传送到的位置; -
ItemResult
:物品结果,获取或遗失某些物品; -
WinResult
:战斗胜利结果; -
LoseResult
:战斗失败结果。 -
其它:包含移动角色,摄像机等,这些我并没有写,你可以添加。
这些结果需要返回一个布尔类型,来表示是否运行成功。
创建类:
[Serializable]
public class Result
{
[XmlIgnore]
public virtual MapEventResultType type
{
get { return MapEventResultType.NoneResult; }
}
public virtual bool Trigger(MapAction action)
{
return true;
}
}
2 剧本结果(Scenario Result)
剧本的结果,是用来触发某些剧情的,自然要使用MapScenarioAction
。
而触发哪段剧情,需要用到剧情标识符。
[Serializable]
public class ScenarioResult : Result
{
[XmlAttribute]
public string flag;
[XmlIgnore]
public override MapEventResultType type
{
get { return MapEventResultType.ScenarioResult; }
}
public override bool Trigger(MapAction action)
{
return action.ScenarioCommand(flag);
}
}
3 创建对象结果(Create Object Result)
我们要在地图创建的对象包含:
-
地图障碍物(MapObstacle)
-
地图职业(MapClass)
- 独有角色
- 杂兵角色
所以,我们要先有其对应的方法才可以。
3.1 创建地图障碍物(Create Map Obstacle)
在MapAction
中,我们创建方法:
protected readonly HashSet<MapObstacle> m_Obstacles =
new HashSet<MapObstacle>();
/// <summary>
/// 创建Obstacle
/// </summary>
/// <param name="prefab"></param>
/// <param name="position"></param>
/// <param name="cmdError"></param>
/// <returns></returns>
public ActionStatus ObjectCommandCreateObstacle(
string prefab,
Vector3Int position,
out string cmdError)
{
// TODO
cmdError = null;
return ActionStatus.Continue;
}
提示:这些方法是在地图事件之前完成的,所以返回值使用了ActionStatus
,这里使用bool
也可以,下同。
首先,我们应该检测其合法性:
-
位置必须在地图内;
-
位置网格中不能存在其它地图对象;
-
创建的
prefab
必须是障碍物。
CellData cellData = map.GetCellData(position);
if (cellData == null)
{
cmdError = string.Format(
"{0} Create Obstacle -> position `{1}` is out of range. prefab: {2}",
"ObjectExecutor",
position.ToString(),
prefab);
return ActionStatus.Error;
}
if (cellData.hasMapObject)
{
cmdError = string.Format(
"{0} Create Obstacle -> the object in position `{1}` is already exist. prefab: {2}",
"ObjectExecutor",
position.ToString(),
prefab);
return ActionStatus.Error;
}
MapObject mapObject = map.CreateMapObject(prefab, position);
if (mapObject == null || mapObject.mapObjectType != MapObjectType.Obstacle)
{
cmdError = string.Format(
"{0} Create Obstacle -> create map object error. prefab: {1}",
"ObjectExecutor",
prefab);
if (mapObject != null)
{
ObjectPool.DespawnUnsafe(mapObject.gameObject, true);
}
return ActionStatus.Error;
}
最后,将它添加到我们的集合中,并返回结果:
m_Obstacles.Add(mapObject as MapObstacle);
cellData.mapObject = mapObject;
cmdError = null;
return ActionStatus.Continue;
3.2 创建地图职业(Create Map Class)
在创建地图职业时,我们需知:
-
关于等级与物品,在独有角色时我们放在了
CharacterInfoConfig
中,而杂兵角色我们将放在这里; -
独有角色时,
id
表示角色id,而在杂兵角色时,它是指职业id; -
玩家阵营不应出现杂兵角色。
创建方法:
/// <summary>
/// 创建MapClass
/// </summary>
/// <param name="attitudeTowards"></param>
/// <param name="roleType"></param>
/// <param name="id"></param>
/// <param name="position"></param>
/// <param name="level"></param>
/// <param name="items"></param>
/// <param name="cmdError"></param>
/// <returns></returns>
public ActionStatus ObjectCommandCreateClass(
AttitudeTowards attitudeTowards,
RoleType roleType,
int id,
Vector3Int position,
int level,
int[] items,
out string cmdError)
{
if (roleType == RoleType.Following && attitudeTowards == AttitudeTowards.Player)
{
cmdError = string.Format(
"{0} Create Class -> role type of player can only be `Unique`",
"ObjectExecutor");
return ActionStatus.Error;
}
CellData cellData = map.GetCellData(position);
if (cellData == null)
{
cmdError = string.Format(
"{0} Create Class -> position `{1}` is out of range. id: {2}",
"ObjectExecutor",
position.ToString(),
id.ToString());
return ActionStatus.Error;
}
if (cellData.hasMapObject)
{
cmdError = string.Format(
"{0} Create Class -> the object in position `{1}` is already exist. id: {2}",
"ObjectExecutor",
position.ToString(),
id.ToString());
return ActionStatus.Error;
}
// TODO
}
然后,关于prefab
也同上,独有角色和杂兵角色是不同的:
RoleModel model = ModelManager.models.Get<RoleModel>();
string prefab;
if (roleType == RoleType.Unique)
{
prefab = model.GetOrCreateClass(model.GetOrCreateCharacter(id).info.classId).info.prefab;
}
else
{
prefab = model.GetOrCreateClass(id).info.prefab;
}
之后,是创建并读取角色:
MapObject mapObject = map.CreateMapObject(prefab, position);
if (mapObject == null)
{
cmdError = string.Format(
"{0} Create Class -> create map object error",
"ObjectExecutor");
return ActionStatus.Error;
}
if (mapObject.mapObjectType != MapObjectType.Class)
{
cmdError = string.Format(
"{0} Create Class -> create map object error, type error. id: {1}",
"ObjectExecutor",
id.ToString());
ObjectPool.DespawnUnsafe(mapObject.gameObject, true);
return ActionStatus.Error;
}
MapClass mapCls = mapObject as MapClass;
if (!mapCls.Load(id, roleType))
{
cmdError = string.Format(
"{0} Run -> load role error. id: {1}, role type: {2}.",
"ObjectExecutor",
id.ToString(),
roleType.ToString());
return ActionStatus.Error;
}
还不要忘记设置其阵营所属,和改变颜色:
mapCls.role.attitudeTowards = attitudeTowards;
mapCls.swapper.SwapColors(GetSwapperColorName(attitudeTowards));
if (roleType == RoleType.Following)
{
mapCls.role.level = Mathf.Max(1, level);
if (items != null && items.Length > 0)
{
ItemModel itemModel = ModelManager.models.Get<ItemModel>();
for (int i = 0; i < items.Length; i++)
{
mapCls.role.AddItem(itemModel.CreateItem(items[i]));
}
}
}
最后,添加到我们的集合中,并返回:
List<MapClass> classes;
if (!m_UnitDict.TryGetValue(attitudeTowards, out classes))
{
classes = new List<MapClass>();
m_UnitDict.Add(attitudeTowards, classes);
}
classes.Add(mapCls);
cellData.mapObject = mapCls;
cmdError = null;
return ActionStatus.Continue;
有了这个方法后,我们来创建帮助方法:
/// <summary>
/// 为独有角色创建MapClass
/// </summary>
/// <param name="attitudeTowards"></param>
/// <param name="id"></param>
/// <param name="position"></param>
/// <param name="cmdError"></param>
/// <returns></returns>
public ActionStatus ObjectCommandCreateClassUnique(
AttitudeTowards attitudeTowards,
int id,
Vector3Int position,
out string cmdError)
{
return ObjectCommandCreateClass(
attitudeTowards,
RoleType.Unique,
id,
position,
0,
null,
out cmdError);
}
/// <summary>
/// 为部下创建MapClass
/// </summary>
/// <param name="attitudeTowards"></param>
/// <param name="id"></param>
/// <param name="position"></param>
/// <param name="level"></param>
/// <param name="items"></param>
/// <param name="cmdError"></param>
/// <returns></returns>
public ActionStatus ObjectCommandCreateClassFollowing(
AttitudeTowards attitudeTowards,
int id,
Vector3Int position,
int level,
int[] items,
out string cmdError)
{
return ObjectCommandCreateClass(
attitudeTowards,
RoleType.Following,
id,
position,
level,
items,
out cmdError);
}
3.3 创建对象结果(Create Object Result)
我们在有了创建方法之后,就可以添加参数了:
[Serializable]
public abstract class MapObjectInfo
{
[XmlIgnore]
public abstract MapObjectType objectType { get; }
[XmlAttribute]
public int x;
[XmlAttribute]
public int y;
}
[Serializable]
public class ObstacleInfo : MapObjectInfo
{
[XmlIgnore]
public override MapObjectType objectType
{
get { return MapObjectType.Obstacle; }
}
[XmlAttribute]
public string prefab;
}
[Serializable]
public abstract class ClassInfo : MapObjectInfo
{
[XmlIgnore]
public override MapObjectType objectType
{
get { return MapObjectType.Class; }
}
[XmlAttribute]
public AttitudeTowards attitudeTowards;
[XmlIgnore]
public abstract RoleType roleType { get; }
[XmlAttribute]
public int id;
}
[Serializable]
public class ClassUniqueInfo : ClassInfo
{
[XmlIgnore]
public override RoleType roleType
{
get { return RoleType.Unique; }
}
}
[Serializable]
public class ClassFollowingInfo : ClassInfo
{
[XmlIgnore]
public override RoleType roleType
{
get { return RoleType.Following; }
}
[XmlAttribute]
public int level;
[XmlAttribute]
public int[] items;
}
这样,我们的主类:
[Serializable]
public class CreateObjectResult : Result
{
[XmlElement("Obstacle", typeof(ObstacleInfo))]
[XmlElement("ClassUnique", typeof(ClassUniqueInfo))]
[XmlElement("ClassFollowing", typeof(ClassFollowingInfo))]
public MapObjectInfo[] objects;
[XmlIgnore]
public string error;
[XmlIgnore]
public override MapEventResultType type
{
get { return MapEventResultType.CreateObjectResult; }
}
public override bool Trigger(MapAction action)
{
if (objects == null || objects.Length == 0)
{
return true;
}
ActionStatus status = ActionStatus.Continue;
for (int i = 0; i < objects.Length; i++)
{
if (objects[i].objectType == MapObjectType.Obstacle)
{
ObstacleInfo info = objects[i] as ObstacleInfo;
status = action.ObjectCommandCreateObstacle(
info.prefab,
new Vector3Int(info.x, info.y, 0),
out error);
}
else if (objects[i].objectType == MapObjectType.Class)
{
ClassInfo info = objects[i] as ClassInfo;
if (info.roleType == RoleType.Unique)
{
status = action.ObjectCommandCreateClassUnique(
info.attitudeTowards,
info.id,
new Vector3Int(info.x, info.y, 0),
out error);
}
else
{
status = action.ObjectCommandCreateClassFollowing(
info.attitudeTowards,
info.id,
new Vector3Int(info.x, info.y, 0),
(info as ClassFollowingInfo).level,
(info as ClassFollowingInfo).items,
out error);
}
}
else
{
error = "CreateObjectResult -> object type can not be `Cursor` or `MouseCursor`.";
}
if (status == ActionStatus.Error)
{
return false;
}
}
return true;
}
}
4 位置结果(Position Result)
[Serializable]
public class PositionResult : Result
{
[XmlAttribute]
public int x;
[XmlAttribute]
public int y;
[XmlIgnore]
public sealed override MapEventResultType type
{
get { return MapEventResultType.PositionResult; }
}
public override bool Trigger(MapAction action)
{
if (action.selectedUnit == null)
{
return false;
}
Vector3Int position = new Vector3Int(x, y, 0);
CellData cellData = action.map.GetCellData(position);
if (cellData == null || cellData.hasMapObject)
{
return false;
}
action.selectedCell.mapObject = null;
action.selectedUnit.UpdatePosition(position);
cellData.mapObject = action.selectedUnit;
return true;
}
}
5 属性结果(Property Result)
[Serializable]
public class PropertyResult : Result
{
[XmlAttribute]
public FightPropertyType propertyType;
[XmlAttribute]
public int value;
[XmlIgnore]
public sealed override MapEventResultType type
{
get { return MapEventResultType.PropertyResult; }
}
public override bool Trigger(MapAction action)
{
if (action.selectedUnit == null)
{
return false;
}
Role role = action.selectedUnit.role;
role.AddFightProperty(propertyType, value);
return true;
}
}
6 物品结果(Item Result)
[Serializable]
public class ItemResult : Result
{
[XmlAttribute]
public int characterId = -1;
[XmlAttribute]
public bool give;
[XmlAttribute]
public int id;
[XmlIgnore]
public sealed override MapEventResultType type
{
get { return MapEventResultType.ItemResult; }
}
public override bool Trigger(MapAction action)
{
Role role;
if (characterId < 0)
{
if (action.selectedUnit == null)
{
return false;
}
role = action.selectedUnit.role;
}
else
{
RoleModel roleModel = ModelManager.models.Get<RoleModel>();
if (!roleModel.ContainsKey(characterId))
{
return false;
}
role = roleModel.GetOrCreateRole(characterId, RoleType.Unique);
}
ItemModel itemModel = ModelManager.models.Get<ItemModel>();
if (give)
{
Item item = itemModel.CreateItem(id);
int index = role.AddItem(item);
if (index == -1)
{
item.Dispose();
return false;
}
}
else
{
int index = -1;
for (int i = 0; i < role.items.Length; i++)
{
if (role.items[i] != null && role.items[i].itemId == id)
{
index = i;
break;
}
}
if (index == -1)
{
return false;
}
Item item = role.RemoveItem(index);
item.Dispose();
}
return true;
}
}
7 胜利结果与失败结果(Win/Lose Result)
在MapAction
中,我们添加一个结束战斗的方法:
/// <summary>
/// 游戏结束
/// </summary>
public void MapEndCommand(int result)
{
if (m_EventCoroutine != null)
{
GameDirector.instance.StopCoroutine(m_EventCoroutine);
m_EventCoroutine = null;
}
GameDirector.instance.StopGameAction();
if (previous != null)
{
ScenarioBlackboard.Set(ScenarioBlackboard.battleMapScene, result);
(previous as ScenarioAction).BattleMapDone(error);
GameDirector.instance.BackGameAction();
MessageCenter.AddListener(GameMain.k_Event_OnSceneLoaded, MapEndCommand_OnSceneLoaded);
if (string.IsNullOrEmpty(nextScene))
{
GameMain.instance.LoadSceneAsync(ScenarioBlackboard.lastScenarioScene, mode: LoadSceneMode.Additive);
}
else
{
GameMain.instance.LoadSceneAsync(nextScene, mode: LoadSceneMode.Additive);
}
}
else
{
Debug.LogError("MapAction -> no previous game action.");
GameDirector.instance.BackGameAction();
}
}
private void MapEndCommand_OnSceneLoaded(string message, object sender, MessageArgs messageArgs, params object[] messageParams)
{
MessageCenter.RemoveListener(GameMain.k_Event_OnSceneLoaded, MapEndCommand_OnSceneLoaded);
OnSceneLoadedArgs args = messageArgs as OnSceneLoadedArgs;
GameMain.instance.SetActiveScene(args.scene.name);
GameMain.instance.UnloadSceneAsync(ScenarioBlackboard.battleMapScene);
ScenarioBlackboard.battleMapScene = string.Empty;
ScenarioBlackboard.mapScript = string.Empty;
GameDirector.instance.RunGameAction();
}
这样,我们触发:
[Serializable]
public class WinResult : Result
{
public sealed override MapEventResultType type
{
get { return MapEventResultType.WinResult; }
}
public override bool Trigger(MapAction action)
{
action.MapEndCommand(1);
return true;
}
}
[Serializable]
public class LoseResult : Result
{
public sealed override MapEventResultType type
{
get { return MapEventResultType.LoseResult; }
}
public override bool Trigger(MapAction action)
{
action.MapEndCommand(-1);
return true;
}
}