第一部分 地图和房间的随机生成及房间进出逻辑
生成地图
在unity中实现枚举变量的多选
对枚举变量进行2的幂次方赋值,在枚举前添加[Flags]
[Flags]
public enum RoomType
{
MinorEnemy=1,
EliteEnemy=2,
Shop=4,
Treasure=8,
RestRoom=16,
Boss=32
}
设计地图配置表,并根据配置表中的信息生成地图
通过列表的方式表现,列表的每个单元代表着一列有几个房间,单元的总数代表地图上一共有多少列
public class MapConfigSo : ScriptableObject
{
public List<RoomBlueprint> roomBlueprints;
}
通过循环实例化每一列的房间,为了防止实例化的房间重叠,还需获取摄像机长宽并计算出间距
private void Awake()
{//获取屏幕长宽,并计算出间距
screemHeight = Camera.main.orthographicSize * 2;
screenWidth = screemHeight * Camera.main.aspect;
columnWidth = screenWidth / (mapConfigSo.roomBlueprints.Count + 1);
}
遍历每一列的同时生成随机数,该随机数就是每一列上的房间数。
每列生成的初始点会在前一列的基础上加间距。每生成一个房间,同一列的下一个房间会在他的基础上y轴向下偏移
public void CreateMap()
{
for (int column = 0; column < mapConfigSo.roomBlueprints.Count; column++)
{
var blueprint = mapConfigSo.roomBlueprints[column];
var amount = Random.Range(blueprint.min, blueprint.max);
var startHeight = screemHeight/2 +screemHeight / (amount + 1);
generatePoint = new Vector3(-screemHeight / 2 + border+column*columnWidth, startHeight, 0);
var newPosition = generatePoint;
var roomGapY = screemHeight / (amount + 1);
//循环当前列的所有房间数量生成房间
for (int i = 0; i < amount; i++)
{
newPosition.y = startHeight - roomGapY * i;
var room = Instantiate(roomPrefab,newPosition,Quaternion.identity, transform);
}
}
}
通过调取材质中的offset实现线的动态效果
private void Update()
{
if (lineRenderer != null)
{
var offset = lineRenderer.material.mainTextureOffset;
offset.x += offsetSpeed * Time.deltaTime;
lineRenderer.material.mainTextureOffset = offset;
}
}
房间之间生成连线
每经过一次循环后就将列表中的房间拷贝给previousColumnRooms,即记录当前列的上一列的房间信息
通过if语句判断上一列是否有数据,即当前列是否为第一列,若不是则执行列与列房间之间的连线操作
if (previousColumnRooms.Count > 0)
{ //创建两个列表的连线
CreateConnections(previousColumnRooms,currebtColumnRooms);
}
遍历第一列的房间并通过ConnectToRandom函数随机返回一个第二列的房间使之相连。
使用哈希表存储第二列已经连接的房间,在第一次连接后遍历第二列,通过哈希表查询没有连接的房间并通过随机函数随机返回一个第一列的房间并与之相连。这样可以保证每一个房间都有链接。
private void CreateConnections(List<Room> column1,List<Room> column2)
{//哈希表中不能有重复的项
HashSet<Room> connectedColumn2Rooms = new ();//第二列中已经被连上的房间
foreach (var room in column1)
{
var targetRoom=ConnectToRandom(room, column2);
connectedColumn2Rooms.Add(targetRoom);
}
foreach (var room in column2)
{
if (!connectedColumn2Rooms.Contains(room))
{
ConnectToRandom(room, column1);
}
}
}
private Room ConnectToRandom(Room room, List<Room> column2)
{
Room targetRoom;
targetRoom = column2[Random.Range(minInclusive: 0, column2.Count)];
//创建房间之间的连线
var line = Instantiate(lineRenderer, transform);
line.SetPosition(0,room.transform.position);
line.SetPosition(1,targetRoom.transform.position);
lines.Add(line);
return targetRoom;
}
可寻址加载
实现随机地图
首先创建一个roomData列表用来存储所有房间的数据
public List<RoomDataSo> roomDataList = new();
private Dictionary<RoomType, RoomDataSo> roomDataDict = new();
遍历房间列表将房间类型和房间数据匹配的添加到字典当中,这样就可以通过房间类型返回房间的数据了
foreach (var roomData in roomDataList)
{
roomDataDict.Add(roomData.roomType,roomData);
}
通过逗号拆分每一列Room Type中的所有房间类型
在options数组中随机选出一个字符串
再将字符串返回为一个房间类型
private RoomType GetRandomRoomType(RoomType flags)
{
string[] options = flags.ToString().Split(',');
string randomOption = options[Random.Range(0, options.Length)];
RoomType roomType = (RoomType)Enum.Parse(typeof(RoomType), randomOption);
return roomType;
}
在随机获取到一个房间类型后
通过Setup根据返回的房间类型生成房间
RoomType newType = GetRandomRoomType(mapConfigSo.roomBlueprints[column].roomType);
room.SetupRoom(column,i,GetRoomData(newType));
点击房间加载对应的场景
跨场景事件
- 创建一个SO文件的基类
因为要创建很多种不同的传递数据的事件,所以直接将基类创建为一个泛型的基类,想传递事件时可以直接更改“T”,这样就可以直接继承BaseEventSO这个基类
public class BaseEventSO<T>:ScriptableObject
{
//事件基类,想传递什么数据类型直接更改尖括号里的就行
[TextArea]
public string description;//事件描述
public UnityAction<T> OnEventRaised;
public string lastSender;//最后一个广播的,从中可以知道谁启动了这个事件
public void RaisEvent(T value,object sender)//启动事件的方法
{
OnEventRaised?.Invoke(value);
lastSender = sender.ToString();
}
}
UnityAction:进行事件的监听并执行多种方法
UnityAction本质上是delegate,且有数个泛型版本(参数最多是4个),一个UnityAction可以添加多个函数(多播委托)
Unity中内置的Unity Action方法
与UnityEvent区分
UnityEvent本质上是继承自UnityEventBase的类,它的AddListener()方法能够注册UnityAction,RemoveListener能够取消注册UnityAction,还有Invoke()方法能够一次性调用所有注册了的UnityAction。UnityEvent也有数个泛型版本(参数最多也是4个),但要注意的一点是,UnityAction的所有带参数的泛型版本都是抽象类(abstract),所以如果要使用的话,需要自己声明一个类继承之,然后再实例化该类才可以使用。
- 创建脚本监听SO事件
同样应是一个泛型的类
public class BaseEventListener<T> : MonoBehaviour
{
public BaseEventSO<T> eventSo;
public UnityEvent<T> response;//反馈启动的事件
private void OnEnable()//注册
{
if (eventSo != null)
{
eventSo.OnEventRaised += OnEventRaised;
}
}
private void OnDisable()//注销
{
if (eventSo != null)
{
eventSo.OnEventRaised -= OnEventRaised;
}
}
private void OnEventRaised(T value)//让response全部都启动,将对应的数值传递进去
{
response.Invoke(value);
}
}
- 创建ObjectEventSO,继承自BaseEventSO
为了传递多种类型的事件,选择传递最基本的object变量,这样可以应用在所有场景当中 - 创建ObjectEventSO对应的监听的内容继承BaseEventListener,同样传递object
- 在Room类中实现点击加载场景的方法
向事件中传递数据类型
private void OnMouseDown()
{
loadRoomEvent.RaisEvent(roomDataSo,this);
}
- 在Persistent场景中创建Scene LoadManger作为监听
添加SceneLoadManager和ObjectEventListener组件 - 在Scene Load Manger中创建获取roomdata的方法
- 写一个单独的Editor的脚本用来把所有注册的方法显示出来:BaseEventsSOEditor
为什么非要创建继承于BaseEventsSOEditor的类才能正常实现? - 创建一个继承于BaseEventsSOEditor的单独的一个Editor代码ObjectEventsSOEditor
BaseEventListener中UnityEvent的response主要是接受动态配置的方法,并在启动的时候将方法挂载到委托类里的OnEventRaised, 通过点击事件触发委托 同时触发配置的动态方法
场景加载
新方法与原异步加载的对比
private async Awaitable LoadSceneTask()
{
var s = currentScene.LoadSceneAsync((LoadSceneMode.Additive));
await s.Task;
if (s.Status == AsyncOperationStatus.Succeeded) //判断异步加载是否成功完成
{
SceneManager.SetActiveScene(s.Result.Scene);
}
}
//卸载场景
private async Awaitable UnLoadSceneTask()
{
await SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene());
}
//加载地图场景
public async void LoadMap()
{
await UnLoadSceneTask();
currentScene = map;
await LoadSceneTask();
}
加载场景的逻辑为:加载地图场景->点击房间后卸载地图场景->加载房间场景
保存地图场景
创建Map LayOut类存储地图的数据,如房间的位置,地图上的线
创建房间数据和线数据并创建列表存储数据
自己创建的类需要序列化才能在窗口中看到,除了Vector3,他需要单独创建一个类型对他进行序列化
[System.Serializable]
public class SerializeVector3
{
public float x, y, z;
public SerializeVector3(Vector3 pos)
{
x = pos.x;
y = pos.y;
z = pos.z;
}
public Vector3 ToVector3()
{
return new Vector3(x, y, z);
}
public Vector2Int ToVector2Int()
{
return new Vector2Int((int)x, (int)y);
}
}
把获取到的Vector拆解为x,y,z,使用ToVector3可以将其返回去,使用ToVector2Int可以返回一个整形数值
在MapGenerator中创建存储地图数据的方法和加载地图数据的方法
存储数据
遍历room列表和lines列表,读取数据并将其存储
private void SaveMap()//存储地图数据
{
mayLayout.MapRoomDataList = new();
//添加所有已经生成的房间
for (int i = 0; i < rooms.Count; i++)
{
var room = new MapRoomData()
{
posX = rooms[i].transform.position.x,
posY = rooms[i].transform.position.y,
colum = rooms[i].column,
line=rooms[i].line,
roomData = rooms[i].roomDataSo,
roomState = rooms[i].roomState
};
mayLayout.MapRoomDataList.Add(room);
}
mayLayout.LineDataList = new List<LinePoaition>();
//添加所有连线
for (int i = 0; i < lines.Count; i++)
{
var line = new LinePoaition()
{
startPos = new SerializeVector3(lines[i].GetPosition(0)),
endPos = new SerializeVector3(lines[i].GetPosition(1)),
};
mayLayout.LineDataList.Add(line);
}
}
加载数据
读取列表中的数据生成房间并加入rooms和lines列表中
private void LoadMap()
{
//读取房间数据生成房间
for (int i = 0; i < mayLayout.MapRoomDataList.Count; i++)
{
var newPos = new Vector3(mayLayout.MapRoomDataList[i].posX, mayLayout.MapRoomDataList[i].posY, 0);
var newRoom = Instantiate(roomPrefab,newPos, Quaternion.identity, transform);
newRoom.roomState = mayLayout.MapRoomDataList[i].roomState;
newRoom.roomDataSo = mayLayout.MapRoomDataList[i].roomData;
newRoom.SetupRoom(mayLayout.MapRoomDataList[i].colum,mayLayout.MapRoomDataList[i].line,mayLayout.MapRoomDataList[i].roomData);
rooms.Add(newRoom);
}
//读取连线
for (int i = 0; i < mayLayout.LineDataList.Count; i++)
{
var line = Instantiate(lineRenderer, transform);
line.SetPosition(0, mayLayout.LineDataList[i].startPos.ToVector3());
line.SetPosition(1,mayLayout.LineDataList[i].endPos.ToVector3());
lines.Add(line);
}
}
进出房间的逻辑
设置只有第一列房间可以进入,其他房间上锁
if (column == 0)
{
room.roomState = RoomState.Attainable;
}
else
{
room.roomState = RoomState.Locked;
}
在连线方法中新增bool值check,用列表记录下每个房间与之相连的房间(check是判断是第一次连接还是确保连接(第二次连接))
if (check)
{
targetRoom.linkTo.Add(new(room.column,room.line));
}
else
{
room.linkTo.Add(new(targetRoom.column,targetRoom.line));
}
每次加载完房间之后Map会被卸载掉,所以需要一个GameManager脚本控制Layout数据,当房间返回时通知更新房间数据,使后一排房间的状态变得可进
因为在房间加载的方法中传入的数据使RoomdataSo类型的数据,而数据中不包含我们所需的Colum和line类,所以将传入的类型更改为Room
改前
改后
在Game Manger中添加更新连线房间状态的的方法
- 将当前选中的房间设置为已访问的
var currentRoom = mapLayout.MapRoomDataList.Find(r => r.colum == roomVector.x && r.line == roomVector.y);
currentRoom.roomState= RoomState.Visited;
- 找出所有的相邻房间并循环遍历修改其状态为不可访问(获胜的和不可访问的要做出差别,如果是不可访问的才执行此代码)
var sameColumnRooms = mapLayout.MapRoomDataList.FindAll(r => r.colum==currentRoom.colum);
foreach (var room in sameColumnRooms)
{
if (room.line != roomVector.y)
{//获胜的和不可访问的要做出差别,如果是不可访问的才执行此代码
room.roomState = RoomState.Locked;
}
- 激活连线的房间
foreach (var link in currentRoom.linkTo)
{
var linkRoom = mapLayout.MapRoomDataList.Find(r => r.colum == link.x&& r.line == link.y);
linkRoom.roomState = RoomState.Attainable;
}
在unity中函数响应事件的类型为object类,为了匹配对应函数响应事件的类型,Update Map Layout Data的传入类型应该为object并在函数中将object转化为Vector2int
根据房间的状态切换对应颜色
(新语法糖)
遇到的问题
如图:UnityAction中没有找到我写的函数
解决:代码中设置T为object,虽然说脚本和物体都可以挂载上去,但只有含有该脚本的物体挂载上去时才能显示出写的脚本
报错invalidcastexception: specified cast is not valid.
object类型强制转换失败
原因是这里传错了数据,将value中的afterRoomLoadEvent改成currentRoomVector就成功了