Unity类杀戮尖塔卡牌游戏开发笔记
日期2024.7.10
技术选型和准备工作
unity选择2023的版本
2D URP核心版本
素材: DOTWeen 和Universal Bronze Icon Pack
Universal Bronze lcon Pack的场景不用导入
player -> 的色彩空间换成Gamma安装Addressables
创建房间的预制体
点击房间之后涉及场景的切换,需要使用 Addressable 来切换场景,因此我们需要通过 Package Manager 来安装 Addressable。
RoomPrefab 是房间的预制体,它下面有张图片叫 Sprite。为了让它能够被点击的时候触发点击事件,所以它上面绑定了一个 Capsule Collider 2D。为了让它在初始化的时候可以显示不同的图标,所以给它绑定了 Room Data。肉鸽卡牌游戏会需要区分在哪一行哪一列,所以需要 Column 和 Line。当我打过房间之后,房间会从可选择状态变为不可选择状态,同时它连线后面的房间会变成可选择状态,所以我们需要使用 Room State 来区分它们。
RoomPrefab
添加碰撞体Capsule
绑定Room
Sprite
中添加Sprite Renderer
Scale 0.6
设置为 Front 层
设计层级
Background Charactor Front
Scripts文件存放所有代码:
Room文件->MonoBBehaviour和ScriptableObject(存放SO文件)
RoomDataSO
using UnityEngine;
using UnityEngine.AddressableAssets;//切换场景
[CreateAssetMenu(fileName = "RoomDataSO", menuName = "Map/RoomDataSO")]
public class RoomDataSO : ScriptableObject
{
public Sprite roomIcon;
public RoomType roomType;//房间类型
public AssetReference sceneToLoad;//需要加载的场景
}
Room
房间图片(关卡图标)被点击的时候进入战斗场景。
public class Room : MonoBehaviour
{
//代表坐标
public int column;//纵向
public int line;//横向
private SpriteRenderer spriteRenderer;
public RoomDataSO roomData; //房间数据
public RoomState roomState;//房间状态
private void Awake()
{
spriteRenderer = GetComponentInChildren<SpriteRenderer>();
}
//测试
// private void Start()
// {
// SetupRoom(0, 0, roomData);
// }
private void OnMouseDown()
{
// 处理点击事件
Debug.Log($"点击了房间:{roomData.roomType}");
}
/// <summary>
/// 外部创建房间时调用配置房间
/// </summary>
/// <param name="colume"></param>
/// <param name="line"></param>
/// <param name="roomData"></param>
public void SetupRoom(int colume, int line, RoomDataSO roomData)
{
this.column = colume;
this.line = line;
this.roomData = roomData;
spriteRenderer.sprite = roomData.roomIcon; //图标
}
Utilities文件
Enums.cs 存放枚举类型
using System;
//房间的类型
public enum RoomType
{
MinorEnemy , //普通敌人
EliteEnemy , //精英敌人
Shop , //商人
Treasure , //宝箱
RestRoom , //休息房间
Boss //Boss
}
//房间的状态
public enum RoomState
{
Locked, //被锁定的
Visited, //以及访问的
Attainable //可以访问的
}
Game Data文件 存放数据
Room Data文件
就是 RoomDataSO生成的文件,
创建了两个 Map,它们分别表示小怪房间和Boss房间
Minor Enemy Boss
好了,别忘了创建房间的预制体RoomPrefab
设计地图配置表和随机地图
添加地图,设置尺寸
锁定Scale 大概是0.74
地图配置文件,规定每一列上面最少和最多有多少个房间,以及这些房间分别可以是哪些类型
MapConfigSO
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "MapConfigSO", menuName = "Map/MapConfigSO")]
public class MapConfigSO : ScriptableObject
{
public List<RoomBlueprint> roomBlueprints;//使用列表形式来代表,就是使用序号来表示
}
//表示房间的地图
[System.Serializable]
public class RoomBlueprint
{
//当前一纵列会出现房间的最小和最大的数量,类型
public int min;
public int max;
public RoomType roomType;
}
Settings文件下生 成map的MapConfigSO
现在要让一纵列出现不同的敌人roomType,可是现在只能单选。
添加7个房间试试
如何在枚举的类型中多选
需要多选的枚举的类型上添加[Flags],而且要设置范围,不然一会可以多选,一会就不能。如下:
[Flags]
public enum RoomType
{
MinorEnemy = 1 << 0,
EliteEnemy = 1 << 1,
Shop = 1 << 2,
Treasure = 1 << 3,
RestRoom = 1 << 4,
Boss = 1 << 5
}
加上<<的,不然就会出现下面的情况:
Map Generafor
挂载Map Generafor代码,右键点击 ReGenerateRoom可以更新地图
获取屏幕的大小,设置关卡位置
如何在枚举的类型中获得一个值
按照屏幕的宽度去除以有多少列,来决定关卡的位置坐标(x,y)
我们现在已经知道屏幕的宽度screenWidth
,也知道了屏幕的高度screenHeight
,
同时还知道一共有多少列mapConfig.roomBlueprints.Count
每一列的宽度columnWidth = screenWidth / mapConfig.roomBlueprints.Count
每列中,会有amount = Random.Range(min, max + 1)
个地点
一列中每行的高度roomGapY = screenHeight / (amount + 1)
那么每一列的上面每个塔的坐标应该是(-screenWidth / 2 + border + columnWidth * colIndex, screenHeight / 2 - roomGapY * (rowIndex + 1))
知道了塔的 X 坐标是-screenWidth / 2 + border + columnWidth * colIndex
,那么我可以让它稍微左右移动一点,显得更随机一些newPosition.x = generatePoint.x + Random.Range(-border / 2, border)
X=屏幕宽度的负值除以2,加上留白的大小
Y=高度除以2,减去房间的间距
最后一个房间BOSS:需要固定 X 轴坐标newPosition.x = screenWidth / 2 - border * 2;
每次运行才生成地图,效率太低了。我们可以新增一个ReGenerateRoom
方法,并给它添加ContextMenu
注解
ContextMenu
注解:
通过使用 ContextMenu 元素,可以向用户呈现一个项列表,这些项指定与特定控件(例如 Button )相关联的命令或选项。 用户通过右键单击控件来显示菜单。 通常,单击 MenuItem 即可打开子菜单或导致应用程序执行某个命令。简单来说就是顶一个自定义鼠标右击菜单。
欧克:
完整的代码MapGenerator.cs:
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using UnityEngine;
public class MapGenerator : MonoBehaviour
{
[Header("地图配置表")]
public MapConfigSO mapConfig;
[Header("地图布局")]
public MapLayoutSO mapLayout;
[Header("预制体")]
public Room roomPrefab;
public LineRenderer linePrefab;
private float screenHeight;//屏幕的高度
private float screenWidth;//屏幕的宽度
private float columnWidth;
private Vector3 generatePoint;//保存房间生成起始点的位置
public float border;//关卡之间的距离
private List<Room> rooms = new List<Room>();
private List<LineRenderer> lines = new List<LineRenderer>();
public List<RoomDataSO> roomDataList = new List<RoomDataSO>();
private Dictionary<RoomType, RoomDataSO> roomDataDict = new Dictionary<RoomType, RoomDataSO>();
private void Awake()
{
// 获取屏幕的高度和宽度
screenHeight = Camera.main.orthographicSize * 2;
screenWidth = screenHeight * Camera.main.aspect;
// 获取一列的宽度
columnWidth = screenWidth / mapConfig.roomBlueprints.Count;
foreach (var roomData in roomDataList)
{
roomDataDict.Add(roomData.roomType, roomData);
}
}
private void Start() {
// 每次启动的时候,从文件中加载 MapLayoutSO
DataManager.instance.LoadFromFile(mapLayout);
}
private void OnEnable() {
if (mapLayout.mapRoomDataList.Count > 0)
{
LoadMap();
}
else
{
CreateMap();
}
}
public void CreateMap()
{
// 创建前一列房间列表
List<Room> previousColumnRooms = new List<Room>();
for (int column = 0; column < mapConfig.roomBlueprints.Count; column++)
{
var blueprint = mapConfig.roomBlueprints[column];
var amount = UnityEngine.Random.Range(blueprint.min, blueprint.max + 1);
var startHeight = screenHeight / 2 - screenHeight / (amount + 1);
generatePoint = new Vector3(-screenWidth / 2 + border + columnWidth * column, startHeight, 0);
var newPosition = generatePoint;
// 创建当前列房间列表
List<Room> currentColumnRooms = new List<Room>();
var roomGapY = screenHeight / (amount + 1);
// 循环当前列的所有房间数量生成房间
for (int i = 0; i < amount; i++)
{
//先判断是否为最后一行,来设置boss的房间
if (column == mapConfig.roomBlueprints.Count - 1)
{
// 判断为最后一列,Boss 房间
newPosition.x = screenWidth / 2 - border * 2;
}
else if (column != 0)
{
//普通关卡x坐标
newPosition.x = generatePoint.x + UnityEngine.Random.Range(-border / 2, border);
}
newPosition.y = startHeight - roomGapY * i;
// 生成房间
var room = Instantiate(roomPrefab, newPosition, Quaternion.identity, transform);
RoomType newType = GetRandomRoomType(mapConfig.roomBlueprints[column].roomType);
// 设置只有第1列房间可以进入其他房间
if (column == 0)
{
room.roomState = RoomState.Attainable;
}
else
{
room.roomState = RoomState.Locked;
}
room.SetupRoom(column, i, GetRoomData(newType));
rooms.Add(room);
currentColumnRooms.Add(room);
}
// 判断当前列是否为第一列,如果不是则连接到上一列
if (previousColumnRooms.Count > 0)
{
// 创建两个列表的房间连线
CreateConnection(previousColumnRooms, currentColumnRooms);
}
previousColumnRooms = currentColumnRooms;
}
SaveMap();
}
private void CreateConnection(List<Room> column1, List<Room> column2)
{
HashSet<Room> connectedColumn2Rooms = new HashSet<Room>();
foreach (var room in column1)
{
var targetRoom = ConnectToRandomRoom(room, column2, false);
connectedColumn2Rooms.Add(targetRoom);
}
// 检查确保 Column2 中所有房间都有连接的房间
foreach (var room in column2)
{
if (!connectedColumn2Rooms.Contains(room))
{
ConnectToRandomRoom(room, column1, true);
}
}
}
/// <summary>
/// 将 room 与 column2 上一个随机的房间进行相连
/// </summary>
/// <param name="room">需要连线的房间</param>
/// <param name="column2">需要被连接的房间列表</param>
/// <param name="check">如果是后面的房间向前连接则为true,如果是前面的房间向后连接则为false</param>
/// <returns></returns>
private Room ConnectToRandomRoom(Room room, List<Room> column2, bool check)
{
Room targetRoom;
targetRoom = column2[UnityEngine.Random.Range(0, column2.Count)];
if (check)
{
// 说明是后面的房间向前连接
targetRoom.linkTo.Add(new Vector2Int(room.column, room.line));
}
else
{
// 说明是前面的房间向后连接
room.linkTo.Add(new Vector2Int(targetRoom.column, targetRoom.line));
}
// 创建房间之间的连线
var line = Instantiate(linePrefab, transform);
// 要确保一下连线的方向是正确的
if (check)
{
// 说明是后面的房间向前连接
line.SetPosition(0, targetRoom.transform.position);
line.SetPosition(1, room.transform.position);
}
else
{
// 说明是前面的房间向后连接
line.SetPosition(0, room.transform.position);
line.SetPosition(1, targetRoom.transform.position);
}
lines.Add(line);
return targetRoom;
}
// 重新生成地图
[ContextMenu("ReGenerateRoom")]
public void ReGenerateRoom()
{
foreach (var room in rooms)
{
Destroy(room.gameObject);
}
foreach (var line in lines)
{
Destroy(line.gameObject);
}
rooms.Clear();
lines.Clear();
CreateMap();
}
private RoomDataSO GetRoomData(RoomType roomType)
{
return roomDataDict[roomType];
}
private RoomType GetRandomRoomType(RoomType flags)
{
// 首先获取出所有房间的名称字符串数组
string[] options = flags.ToString().Split(',');
// 然后从数组中随机取出一个
string randomOption = options[UnityEngine.Random.Range(0, options.Length)];
// 然后根据字符串获取到对应的 RoomType
return (RoomType)Enum.Parse(typeof(RoomType), randomOption);
}
private void SaveMap()
{
mapLayout.mapRoomDataList = new List<MapRoomData>();
// 添加所有已经生成的房间
for (int i = 0; i < rooms.Count; i++)
{
var room = new MapRoomData()
{
posX = rooms[i].transform.position.x,
posY = rooms[i].transform.position.y,
column = rooms[i].column,
line = rooms[i].line,
roomType = rooms[i].roomData.roomType,
roomState = rooms[i].roomState,
linkTo = rooms[i].linkTo
};
mapLayout.mapRoomDataList.Add(room);
}
mapLayout.linePositionList = new List<LinePosition>();
// 添加所有连线
for (int i = 0; i < lines.Count; i++)
{
var line = new LinePosition()
{
startPos = new SerializeVector3(lines[i].GetPosition(0)),
endPos = new SerializeVector3(lines[i].GetPosition(1))
};
mapLayout.linePositionList.Add(line);
}
// 存储到文件中
DataManager.instance.SaveToFile(mapLayout);
}
private void LoadMap()
{
// 读取房间数据生成房间
for (int i = 0; i < mapLayout.mapRoomDataList.Count; i++)
{
var newPos = new Vector3(mapLayout.mapRoomDataList[i].posX, mapLayout.mapRoomDataList[i].posY, 0);
var newRoom = Instantiate(roomPrefab, newPos, Quaternion.identity, transform);
newRoom.roomState = mapLayout.mapRoomDataList[i].roomState;
RoomDataSO roomDataSO = roomDataDict[mapLayout.mapRoomDataList[i].roomType];
newRoom.SetupRoom(mapLayout.mapRoomDataList[i].column, mapLayout.mapRoomDataList[i].line, roomDataSO);
newRoom.linkTo = mapLayout.mapRoomDataList[i].linkTo;
rooms.Add(newRoom);
}
// 读取连线
for (int i = 0; i < mapLayout.linePositionList.Count; i++)
{
var line = Instantiate(linePrefab, Vector3.zero, Quaternion.identity, transform);
line.SetPosition(0, mapLayout.linePositionList[i].startPos.ToVector3());
line.SetPosition(1, mapLayout.linePositionList[i].endPos.ToVector3());
lines.Add(line);
}
}
}
生成房间之间的连接线
画线使用了 LineRenderer
LineRenderer 创建了一个材质
材质中有个 Tilling 变量,它表示线段的密度,当这个数值越大的时候,虚线之间空隙的间隔就越小
材质中有个 Offset 变量,它表示线段的偏移,可以修改这个值使线条动起来。
创建后保存预制体
实现房间之间的连线
修改 MapGenerator 代码,给它添加 LinePrefab
创建房间的时候,需要将这一列的房间添加到当前列房间列表
中。
该列创建完房间之后,看看当前列是否是第0列,如果不是第0列,需要在当前列房间列表
和上一列房间列表
之间创建连线
创建连线的方法实现如下,遍历上一列房间列表
的每个房间,然后和随机的当前列房间列表
的房间进行相连,连接的方法是创建出一个LinePrefab
,然后修改它第0点和第1点的position
,创建出来的连线需要记录到List
当中,以便重新生成地图的时候需要删除这些连线。每次连接过一个当前列房间列表
的房间之后,需要记录一下,等上一列房间列表
的房间都遍历完毕之后,再遍历当前列房间列表
中没有被访问的房间,进行反向连接。这样就能确保两列之间所有的房间都有连线。
当前列房间列表`也遍历完毕之后,需要把`当前列房间列表`变为`上一列房间列表
让连线动起来
创建材质:Material(材质)ConnecrLine
主要是改变材质的Offset的x值让线条动起来。
Line,挂载到给LinePrefab
上
public class Line : MonoBehaviour
{
public LineRenderer lineRenderer;
public float offsetSpeed = 0.1f;
//在Update方法里面修改LineRenderer的offset.x
private void Update()
{
if (lineRenderer != null)
{
// 获取当前纹理偏移
var offset = lineRenderer.material.mainTextureOffset;
offset.x -= offsetSpeed * Time.deltaTime;
lineRenderer.material.mainTextureOffset = offset;
}
}
}