地图编辑器
Introduction
游戏开发过程中,我们搭建关卡时需要一个工具去支持摆放场景物件和数据埋点编辑,从而支持快速搭建关卡和策划数据埋点。这一工具正是本章节需要实现的地图编辑器。
地图编辑器
需求
实现一个工具,首先第一步,我们还是需要理清需求:
- 纯Editor地图编辑器,用于场景编辑和数据埋点。
- 支持自定义场景对象数据和自由摆放场景对象进行场景编辑,场景对象支持静态摆放和动态创建两种。
- 支持自定义埋点数据和自由摆放埋点数据进行数据埋点编辑。
- 支持自定义调整场景大小以及场景地形指定和大小自动适配。
- 场景数据和埋点数据支持导出自定义数据格式(比如Lua,Json等)。
- 同一个场景支持编辑多个场景编辑和数据埋点。
设计实现
接下来针对前面提到的需求,我们一一分析我们应该用什么方案和技术来实现。
实现思路:
- 地图编辑器主要由地图编辑器配置窗口和地图编辑器挂在操作脚本Inspector组成。
- 地图编辑器编辑数据分为两大类(1. 地图编辑 2. 数据埋点)。
- 继承EditorWindow实现场景编辑和数据埋点的基础数据配置编辑。
- 继承Editor自定义Inspector面板实现纯单个场景编辑和数据埋点操作。
- 地图编辑操作通过挂在脚本(Map.cs)的方式作为单个场景编辑和数据埋点单位,从而实现单个场景多个场景编辑和数据埋点支持。
- 场景对象编辑采用直接创建实体GameObject的方式,从而实现场景编辑完成后的场景可直接用于作为场景使用。
- 场景对象编辑通过自定义Inspector面板实现快速删除和创建场景对象GameObject实现场景对象的编辑。
- 数据埋点采用Gizmos(Monobehaviour:OnDrawGizmos()),Handles(Editor.OnSceneGUI())实现可视化编辑对象和相关数据显示,自定义场景大小配置网格显示也是用Gizmos实现。
- 地图编辑器配置窗口用于配置基础的场景数据和埋点数据配置,Map.cs的挂在脚本通过自定义数据结构和自定义面板显示实现自定义数据配置。
- 场景静态对象相关数据通过挂在MapObjectDataMono脚本存储相关对象数据。
- 导出前通过Map.cs存储的数据构建自定义数据(MapExport.cs)实现自定义数据导出
- 大地图未来有需求可以做成所有静态对象通过导出数据根据逻辑加载的方式实现按需加载。
核心思路和技术实现方案都在上面介绍了,这里就不一一介绍代码实现了,这里只放部分关键代码,让我们直接实战看效果,需要源码的直接在文章末尾Github链接去取即可。
配置窗口
配置窗口主要负责实现对地图编辑对象,地图编辑埋点的基础数据定义,一些相关操作按钮和全局配置。
配置定义
地图配置数据结构定义:
1 | /// <summary> |
地图对象配置定义:
1 | /// <summary> |
地图埋点配置定义:
1 | /// <summary> |
配置定义设计如下:
- MapSetting是地图编辑器配置数据结构定义,继承至ScriptableObject,保存到Editor
- MapObjectSetting是地图对象的所有编辑器配置结构定义。MapObjectConfig是地图对象编辑器单条配置结构定义。MapObjectType是地图对象类型定义。
- MapDataSetting是地图埋点的所有编辑器配置结构定义。MapDataConfig是地图埋点编辑器单条配置结构定义。MapDataType是地图埋点类型定义。
配置窗口
可以看到在地图编辑器窗口主要分为4个区域:
- 快捷按钮区域
- 自定义地图基础数据区域(e.g. 默认地图宽高)
- 地图对象配置区域
- 地图埋点配置区域
以上配置数据会成为我们后面操作编辑Inspector里用户可以添加操作的基础数据来源,详细代码参见MapEditorWindow.cs。
快捷按钮区域
效果图:
可以看到目前提供了三个快捷按钮:
- 保存地图配置数据 – 用于确保我们在配置窗口的数据配置修改保存到本地ScriptableObject Asset
- 打开地图编辑场景 – 用于帮助我们快速打开辅助地图编辑的场景
- 快速选中地图地编对象 – 用于帮助我们快速选中当前场景里第一个带Map.cs脚本的对象,方便快速进入Map.cs的Inspector操作面板操作
- 一键导出所有地图 – 用于一键导出所有编辑好的地图埋点数据
- 一键烘焙导出所有地图 – 用于一键烘焙所有场景和导出所有编辑好的地图埋点数据
自定义地图数据区域
效果图:
自定义数据目前只支持了默认创建地图编辑宽高配置(挂在Map.cs脚本时默认创建的地图横向竖向大小数据来源)。
未来扩展更多基础配置数据在这里添加。
地图对象配置区域
效果图:
地图对象配置设计如下:
以UID作为唯一配置Id标识(不允许重复),此Id会作为我们编辑器地图对象配置数据读取的依据。
通过MapObjectType指定地图对象类型。
通过定义ConfId(关联配置Id)实现关联游戏内动态对象Id的功能。
地图对象编辑通过定义Asset实现指定自定义预制件Asset作为实体对象的资源对象。
描述用于方便用户自定义取名,方便识别不同的地图对象配置。
Note:
- 需要参与导出的地图基础配置数据如果修改了(比如关联配置Id),对应用到此基础配置数据的关卡都需要重新导出。
- MapObjectType需要代码自定义扩展后,编辑器才有更多选项
地图埋点配置区域
效果图:
地图埋点配置设计如下:
- 以UID作为唯一配置Id标识(不允许重复),此Id会作为我们编辑器地图埋点配置数据读取的依据。
- 通过MapDataType指定地图埋点类型。
- 通过定义ConfId(关联配置Id)实现关联游戏内动态对象Id的功能。
- 场景球体颜色用于配置地图埋点的GUI球体绘制颜色配置。
- 初始旋转用于配置地图埋点添加时的初始旋转配置(方便自定义大部分用到的初始宣旋转数据)
- 描述用于方便用户自定义取名,方便识别不同的地图埋点配置。
Note:
- MapDataType需要代码自定义扩展后,编辑器才有更多选项
操作编辑Inspector
操作编辑Inspector主要负责实现对地图编辑对象和地图编辑埋点的一些相关操作面板,可视化GUI数据绘制,地图编辑自定义基础数据配置以及一些快捷操作按钮,详细代码参见Map.cs和MapEditor.cs。
基础数据配置区域
基础数据配置区域用于GUI开关,地图大小,地图起始位置,自定义地形等配置。
效果图:
自定义配置区域介绍:
- 场景GUI总开关 – 所有绘制GUI的总开关
- 地图线条GUI开关 – 控制地图N*N的线条GUI绘制开关
- 地图对象场景GUI开关 – 控制地图对象相关的GUI绘制开关
- 地图埋点场景GUI开关 – 控制地图埋点相关的GUI绘制开关
- 地图对象创建自动聚焦开关 – 控制地图对象创建后是否需要自动聚焦选中
- 地图横向大小和地图纵向大小 – 用于控制我们需要编辑的关卡地图的大小,会影响默认地图或自定义地图的大小和平铺
- 游戏地图起始位置 – 用于控制关卡的起始位置偏移,方便支持自定义不同起始位置的关卡设置
快捷操作按钮区域
快捷操作按钮区域用于支持自定义功能。
效果图:
快捷按钮功能介绍:
- 拷贝NavMesh Asset按钮 – NavMeshSurface默认烘焙Asset保存在对应场景同层目录,此按钮用于快速拷贝对应Asset到对应关卡预制件同层目录
- 一键重创地图对象按钮 – 用于我们更新了某些已经创建好的静态地图对象(脱离预制件关联)相关资源后一键确保使用最新资源创建
- 导出地图数据按钮 – 用于完成我们的关卡数据序列化导出
- 保存关卡数据 – 用于独立保存关卡埋点相关数据(支持自定义名字,默认和预制件同名)
- 一键烘焙拷贝导出 – 用于一键完成恢复动态对象+烘培寻路+拷贝寻路+清除动态对象+导出地图数据操作
Note:
- 场景对象是否参与寻路烘培,通过修改预制件Layer和NavMeshSurface的寻路烘培的Layer决定
- 大部分操作在实体对象做成预制件后需要进入预制件编辑模式,所以部分操作会根据脚本所在实体对象情况决定是否自动进入预制件编辑模式
地图对象编辑区域
效果图:
地图对象数据定义:
1 | /// <summary> |
地图对象编辑通过选择地图对象类型和地图对象选择决定要添加的地图对象配置。
地图对象编辑和地图对象选择后面的**+默认是添加到地图对象数据列表尾部**。
地图对象数据列表后的操作可以对已经配置的地图对象数据进行相关操作,此处的**+号是将选择的地图对象类型和地图对象选择插入的当前数据位置**。
Note:
- 地图对象可以通过地图对象配置面板配置的关联id导出给程序实现动态对象的配置关联
- 地图对象的数据关联是通过挂在MapObjectDataMono.cs脚本实现
- 未来如果地图对象要想实现按需加载可以通过导出地图对象数据给程序动态加载实现,相关代码参考MapObjectExport.cs相关代码(不分代码注释着)
地图埋点编辑区域
效果图:
地图埋点数据定义:
1 | /// <summary> |
地图埋点编辑通过选择地图埋点类型和地图埋点选择决定要添加的地图埋点配置。
地图埋点编辑和地图埋点选择后面的**+默认是添加到地图埋点数据列表尾部**。
地图埋点数据列表后的操作可以对已经配置的地图埋点数据进行相关操作,此处的**+号是将选择的地图埋点类型和地图对象选择插入的当前数据位置**。
地图埋点支持批量操作位置,通过勾选批量选项决定哪些地图埋点数据要一起操作位置,然后操作(拖拽或者输入坐标)勾选了批量的地图埋点对象的位置,可以实现一起修改勾选了批量操作的地图埋点位置。
一键清除批量勾选按钮 – 用于快速取消所有已勾选批量的地图埋点数据
Note:
- 地图数据的编辑和导出是按Map.cs脚本为单位。
- 地图埋点数据时通过Editor GUI(e.g. Handles和Gimoz)绘制的
- 地图埋点的位置调整需要按住W按键,旋转调整需要按住E按键
- 地图埋点支持配置自定义数据,这部分可以参考Monster埋点的自定义数据配置
- 地图埋点通过地图埋点配置面板配置的关联id导出给程序实现地图埋点的配置关联
- 自定义数据埋点可视化可以通过扩展MapEditor.cs实现自定义绘制
寻路烘培
目前场景编辑是以Map.cs为单位。所以寻路烘培目前也是按Map.cs所在预制件为单位。
通过每个Map.cs所在GameObject挂在NavMeshSurface组件实现对当前GameObject的寻路烘培。
效果图:
Note:
- NavMeshSurface设置Collect Objects为Children(只有子节点参与烘培),参与烘培的Include Layers设置成自定义的实现是否参与烘培的Layer规则。
数据导出
数据导出是通过点击导出地图数据按钮或者一键烘培拷贝导出按钮实现的。
为了支持多种不同数据格式的导出,在数据导出之前我进行导出数据的定义和抽象。
地图导出数据结构定义:
1 | /// <summary> |
地图对象导出数据基类定义:
1 | /// <summary> |
地图埋点导出数据基类定义:
1 | /// <summary> |
怪物埋点数据导出定义:
1 | /// <summary> |
地图导出数据预览Level1.json:
1 | { |
上面的数据正对应我们导出填充的MapExport数据结构。
MapExport导出数据构建是通过GameMapEditorUtilities.GetMapExport()通过Map脚本获取所有相关数据去构建MapExport实现导出数据填充构建的。
Note:
- 因为静态地图对象是跟随预制件一起加载的,所以暂时没导出地图对象数据,有需要可以自行扩展支持动态地图对象导出和加载
- 扩展自定义配置数据需要自行扩展或继承MapObjectData或MapData和MapEditor.cs面板显示
- 扩展自定义导出数据需要自行扩展或继承BaseMapDataExportt定义
- 自定义地图埋点数据导出通过继承BaseMapDataExport定义
实战
新增地图埋点数据
以新增地图埋点类型MapDataType.Monster为例,怪物有两个DIY属性MonsterCreateRadius (怪物创建半径)和MonsterActiveRadius(怪物警戒半径)
新增地图埋点数据显示
增加地图埋点类型定义(MapDataType.cs)
1
2
3
4
5
6
7
8
9/// <summary>
/// MapDataType.cs
/// 地图数据埋点类型枚举
/// </summary>
public enum MapDataType
{
PlayerSpawn = 0, // 玩家出生点位置数据
Monster = 1, // 怪物数据
}新增地图埋点数据定义(MonsterMapData.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54using System;
using UnityEngine;
namespace MapEditor
{
/// <summary>
/// MonsterMapData.cs
/// 怪物地图埋点数据
/// </summary>
[ ]
public class MonsterMapData : MapData
{
/// <summary>
/// 怪物创建半径
/// </summary>
[ ]
public float MonsterCreateRadius = 4;
/// <summary>
/// 怪物警戒半径
/// </summary>
[ ]
public float MonsterActiveRadius = 3;
public MonsterMapData(int uid) : base(uid)
{
}
public MonsterMapData(int uid, Vector3 position, Vector3 rotation, float monsterCreateRadius, float monsterActiveRadius)
: base(uid, position, rotation)
{
MonsterCreateRadius = monsterCreateRadius;
MonsterActiveRadius = monsterActiveRadius;
}
/// <summary>
/// 复制自定义数据
/// </summary>
/// <param name="sourceMapData"></param>
/// <returns></returns>
public override bool CopyCustomData(MapData sourceMapData)
{
if(!base.CopyCustomData(sourceMapData))
{
return false;
}
var realSourceMapData = sourceMapData as MonsterMapData;
MonsterCreateRadius = realSourceMapData.MonsterCreateRadius;
MonsterActiveRadius = realSourceMapData.MonsterActiveRadius;
return true;
}
}
}新增编辑器操作添加怪物地图埋点数据(MapUtilities.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/// <summary>
/// 创建指定地图埋点数据类型,指定uid和指定位置的埋点数据
/// </summary>
/// <param name="mapDataType"></param>
/// <param name="uid"></param>
/// <param name="position"></param>
/// <param name="rotation"></param>
/// <param name="monsterCreateRadius"></param>
/// <param name="monsterActiveRadius"></param>
/// <returns></returns>
public static MapData CreateMapDataByType(MapDataType mapDataType, int uid, Vector3 position, Vector3 rotation, float monsterCreateRadius, float monsterActiveRadius)
{
if (mapDataType == MapDataType.Monster)
{
return new MonsterMapData(uid, position, rotation, monsterCreateRadius, monsterActiveRadius);
}
else if (mapDataType == MapDataType.PlayerSpawn)
{
return new PlayerSpawnMapData(uid, position, rotation);
}
******
else
{
Debug.LogWarning($"地图埋点类型:{mapDataType}没有创建自定义类型数据,可能不方便未来扩展!");
return new MapData(uid, position, rotation);
}
}新增怪物编辑器GUI显示数据定义(MapEditorUtilities.cs)
1
2
3
4
5
6
7
8
9/// <summary>
/// 地图埋点类型UI类型显示数据列表
/// </summary>
private static readonly List<MapDataTypeDisplayData> MapDataTypeDisplayDatas = new List<MapDataTypeDisplayData>()
{
new MapDataTypeDisplayData(MapDataType.PlayerSpawn, MapFoldType.PlayerSpawnMapDataFold, "玩家出生点", Color.yellow),
new MapDataTypeDisplayData(MapDataType.Monster, MapFoldType.MonsterMapDataFold, "怪物", Color.magenta),
******
};新增怪物自定义数据UI显示数据定义(MapUIType.cs)
1
2
3
4
5
6
7
8
9
10/// <summary>
/// MapUIType.cs
/// 地图数据UI类型
/// </summary>
public enum MapUIType
{
*******
MonsterCreateRadius = 12, // 怪物创建半径UI
MonsterActiveRadius = 13, // 怪物警戒半径UI
}新增怪物自定义类型数据UI显示相关数据定义(MapEditorUtilities.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/// <summary>
/// 地图埋点类型和UI显示类型Map<地图埋点类型,<地图UI显示类型,是否显示>>
/// Note:
/// 所有需要显示的折叠数据都在这里定义相关UI是否显示
/// 通用必显示的不同定义在这里,详情参见CommonGameMapUITypeMap
/// 所有MapDataType类型必定会自动初始化到MapFoldAndUITypeMap,没有DIY UI显示的可以不定义在这
/// </summary>
private static Dictionary<MapDataType, Dictionary<MapUIType, bool>> MapFoldAndUITypeMap = new Dictionary<MapDataType, Dictionary<MapUIType, bool>>()
{
{
MapDataType.Monster, new Dictionary<MapUIType, bool>()
{
{MapUIType.MonsterCreateRadius, true},
{MapUIType.MonsterActiveRadius, true},
}
},
};
/// <summary>
/// 地图UI类型显示数据列表
/// Note:
/// 标题和属性默认按照这里定义的顺序显示
/// </summary>
public static readonly List<MapUITypeDisplayData> MapUITypeDisplayData = new List<MapUITypeDisplayData>()
{
******
new MapUITypeDisplayData(MapUIType.MonsterCreateRadius, "MonsterCreateRadius", "创建半径", MapEditorConst.InspectorDataMonsterCreateRadiusUIWidth, MapStyles.TabMiddleStyle),
new MapUITypeDisplayData(MapUIType.MonsterActiveRadius, "MonsterActiveRadius", "警戒半径", MapEditorConst.InspectorDataMonsterActiveRediusUIWidth, MapStyles.TabMiddleStyle),
******
};新增属性GUI显示代码(MapEditor.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/// <summary>
/// 绘制单个地图埋点属性,索引,地图埋点类型和现实数据的数据
/// </summary>
/// <param name="mapDataProperty"></param>
/// <param name="mapDataIndex"></param>
/// <param name="mapDataType"></param>
/// <param name="mapUITypeDisplayData"></param>
private void DrawOneMapDataPropertyByData(SerializedProperty mapDataProperty, int mapDataIndex, MapDataType mapDataType, MapUITypeDisplayData mapUITypeDisplayData)
{
******
else if (mapUIType == MapUIType.MonsterCreateRadius)
{
DrawFloatChangeMapDataProperty(mapDataIndex, property, propertyName, MapEditorConst.InspectorDataMonsterCreateRadiusUIWidth);
}
else if (mapUIType == MapUIType.MonsterActiveRadius)
{
DrawFloatChangeMapDataProperty(mapDataIndex, property, propertyName, MapEditorConst.InspectorDataMonsterActiveRediusUIWidth);
}
******
}至此我们成功添加了自定义埋点数据类型和DIY数据显示,接下来就是数据导出。
Note:
- 公共UI类型是否显示定义在MapEditorUtilities.CommonMapUITypeMap里,自定义UI类型是否显示定义在MapEditorUtilities.MapFoldAndUITypeMap里
新增地图埋点数据导出
新增怪物导出数据定义(MonsterMapDataExport.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/// <summary>
/// MonsterMapDataExport.cs
/// 怪物地图埋点数据导出
/// </summary>
[ ]
public class MonsterMapDataExport : BaseMapDataExport
{
/// <summary>
/// 怪物创建半径
/// </summary>
public float MonsterCreateRadius;
/// <summary>
/// 怪物警戒半径
/// </summary>
public float MonsterActiveRadius;
public MonsterMapDataExport(MapDataType mapDataType, int confId, Vector3 position, Vector3 rotation, float monsterCreateRadius, float monsterActiveRadius)
: base(mapDataType, confId, position, rotation)
{
MonsterCreateRadius = monsterCreateRadius;
MonsterActiveRadius = monsterActiveRadius;
}
}导出数据添加怪物导出数据定义(MapExport.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/// <summary>
/// MapExport.cs
/// 地图导出数据结构定义(统一导出结构定义,方便支持导出不同的数据格式)
/// </summary>
[ ]
public class MapExport
{
******
/// <summary>
/// 所有怪物导出数据列表
/// </summary>
public List<MonsterMapDataExport> ALlMonsterMapDatas = new List<MonsterMapDataExport>();
******
}构建导出数据并填充到怪物导出数据列表里(MapExportEditorUtilities.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50/// <summary>
/// 指定地图埋点数据列表更新地图导出数据
/// </summary>
/// <param name="mapExport"></param>
/// <param name="map"></param>
private static void UpdateMapExportByMapDatas(MapExport mapExport, Map map)
{
if(map?.MapDataList == null)
{
return;
}
var mapDatas = map.MapDataList;
var mapDataTypeDatasMap = GetMapDataTypeDatas(mapDatas);
******
List<MapData> monsterDatas;
if (mapDataTypeDatasMap.TryGetValue(MapDataType.Monster, out monsterDatas))
{
foreach (var monsterData in monsterDatas)
{
var monsterMapDataExport = GetMonsterMapDataExport(monsterData);
mapExport.ALlMonsterMapDatas.Add(monsterMapDataExport);
}
}
******
}
/// <summary>
/// 获取指定地图埋点数据的地图埋点怪物导出数据
/// </summary>
/// <param name="mapData"></param>
/// <returns></returns>
private static MonsterMapDataExport GetMonsterMapDataExport(MapData mapData)
{
if (mapData == null)
{
Debug.LogError("不允许获取空地图埋点数据的地图埋点怪物导出数据失败!");
return null;
}
var mapDataUID = mapData.UID;
var mapDataConfig = MapSetting.GetEditorInstance().DataSetting.GetMapDataConfigByUID(mapDataUID);
if (mapDataConfig == null)
{
Debug.LogError($"找不到地图埋点UID:{mapDataUID}的配置,获取地图埋点怪物导出数据失败!");
return null;
}
var monsterMapData = mapData as MonsterMapData;
var monsterCreateRadius = monsterMapData.MonsterCreateRadius;
var monsterActiveRadius = monsterMapData.MonsterActiveRadius;
return new MonsterMapDataExport(mapDataConfig.DataType, mapDataConfig.ConfId, mapData.Position, mapData.Rotation, monsterCreateRadius, monsterActiveRadius);
}然后MapExport导出时,数据就会序列化到Json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59{
"MapData": {
"Width": 8,
"Height": 40,
"StartPos": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"BirthPos": [
{
"x": 3.0,
"y": 0.0,
"z": 3.0
}
],
"GameVirtualCameraInitPos": {
"x": 3.0,
"y": 15.0,
"z": -10.0
},
"GridSize": 0.0
},
"ALlMonsterMapDatas": [
{
"MapDataType": 1,
"ConfId": 1,
"Position": {
"x": 3.0,
"y": 0.0,
"z": 15.0
},
"Roation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"MonsterCreateRadius": 8.0,
"MonsterActiveRadius": 5.0
},
{
"MapDataType": 1,
"ConfId": 2,
"Position": {
"x": 3.0,
"y": 0.0,
"z": 20.0
},
"Roation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"MonsterCreateRadius": 8.0,
"MonsterActiveRadius": 5.0
}
],
******
}
新增地图埋点数据读取
完成埋点数据导出后,接下来就是数据读取(MapGameManager.cs)
1 | /// <summary> |
最后通过MapGameManager.Singleton.LevelConfig.ALlMonsterMapDatas即可访问导出的怪物埋点数据了。
摄像机可见范围可视化
摄像机的可见范围计算原理:
- 不考虑摄像机是正交还是透视,通过将屏幕四个角映射到世界坐标系(屏幕坐标到世界坐标),得到从摄像机到摄像机四个角的射线数据。
1 | /// <summary> |
- 得到屏幕四个角映射的射线数据后,利用射线和平面的交叉计算得到指定平面交叉的点就能得到我们要的平面照射区域
1 | /// <summary> |
1 | /// <summary> |
从上面可以看到无论是areaPointsList还是rectPointList都返回了5个顶点数据,第五个是屏幕中心映射的点。
之所以返回矩形的映射区域,是为了简化后面透视摄像机梯形判定顶点复杂度,简化成AABB和点的交叉判定。
- 得到了屏幕映射的顶点数据,通过构建线条数据,我们就能利用GUI相关接口画出可视化可见区域了
1 | /// <summary> |
摄像机照射区域可视化:
可以看到黄色是远截面照射区域,深绿色是近截面照射区域,浅绿色是摄像机近截面换算成矩形后的区域,红色线条是摄像机近截面和远截面射线,
Note:
- 屏幕4个顶点计算顺序依次是左下,左上,右上,右下
摄像机可见范围动态创建和销毁
前面我们已经成功得到了摄像机照射范围映射到指定平面映射顶点数据,接下来就是通过埋点对象数据的位置结合摄像机映射数据,通过判定AABB和点的交互决定指定地图埋点数据是否可见,从而决定是否创建或移除相关地图埋点数据的对象数据。
1 | /// <summary> |
上面的代码已经成功检测到地图埋点位置在可视域就创建,不在可视域就销毁的逻辑。
从截图可以看到近处的两个红色怪物埋点因为超出了摄像机区域后被销毁了,而远处两个宝箱和陷阱因为进入摄像机照射区域而创建显示了。
学习总结
- EditorWindow配置编辑器基础可配置数据,Inspector面板提供地图编辑器操作入口和数据序列化存储
- 地图对象通过序列化位置+旋转+碰撞等数据可以实现场景编辑后一键更新所有场景对象效果
- 地图对象数据存储可以挂在Mono脚本或自定义数据导出实现场景对象相关配置数据存储和导出
- 数据埋点显示和操作通过Gizmo和Handles可以实现自己设计的操作交互和显示
- 摄像机可视域的计算和判定基本上就是数学上的运算