Unity类杀戮尖塔卡牌游戏开发笔记

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;
        }
    }

}
  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity卡牌游戏开发通常需要以下几个步骤。首先,需要确定需求,包括需要一个存放Sprite的数组用于赋值,并修改代码以随机生成卡牌的封面。其次,新建一个空物体并创建一个名为SceneController的脚本。在这个脚本中,我们可以定义卡牌的预制体和一些参数,如最大卡牌数、每行最大卡牌数和相邻卡牌的间隔。接着,在createCards()方法中使用Instantiate函数实例化卡牌预制体,并按照设定的间隔位置将卡牌分布在场景中。最后,在Start()方法中调用createCards()方法来生成卡牌[2]。 在这个过程中,我们选择了使用SceneController动态生成一个数组来给不同的卡牌赋值的方法。这样做的好处是不同的卡牌只有图片一个属性不同,通过动态生成数组来给卡牌赋值可以避免一个个手动拖拽预制体到对应的位置上赋值的麻烦。这种方法简化了开发过程,提高了开发效率。 总结起来,Unity卡牌游戏开发可以通过确定需求、修改代码以随机生成卡牌封面、创建一个SceneController脚本来管理卡牌的生成和赋值。这样的开发流程能够简化开发过程并提高开发效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Unity实现简单卡牌游戏框架](https://blog.csdn.net/LuLou_/article/details/115191300)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值