[Unity] 棋盘上存在多个维护自己的空位列表的区域的实现

108 阅读3分钟

要求一个地图上具有若干个区域,每个区域由左下角坐标和右上角坐标定义

SpawnRegionSetting.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "SO_SpawnRegionSetting",
    menuName = "Chess/SpawnRegionSetting")]
public class SpawnRegionSetting : ScriptableObject
{
    public List<SpawnRegion> regions;
}

[Serializable]
public class SpawnRegion
{
    public Vector2Int bottomleft;

    public Vector2Int upright;

    public bool IsInRegion(Vector2 pos)
    {
        if (pos.x >= bottomleft.x &&
            pos.y >= bottomleft.y &&
            pos.x <= upright.x &&
            pos.y <= upright.y)
            return true;

        return false;
    }
}

需要有一个脚本控制各个区域,每个区域对应一个空位列表

有一个统一的函数添加或者移除空位,因为各个区域之间可能会重叠,所以这个函数需要负责将空位从各个区域的空位列表添加或者移除

为了获得空位,也需要直到哪个区域里面存在空位

RegionsController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Universal;
using Random = UnityEngine.Random;

public class RegionsController : Singleton<RegionsController>
{
    [Header("Spawn Setting")]
    
    public SpawnRegionSetting regionSetting;

    [SerializeField, ReadOnly]
    public List<Positions> emptyPosInRegions = new List<Positions>();

    [SerializeField]
    private int showEmptyPosInRegionsIndex = -1;
    
    protected override void Awake()
    {
        base.Awake();
        
        for (int i = 0; i < regionSetting.regions.Count; i++)
        {
            emptyPosInRegions.Add(new Positions());
        }
    }
    
    /// <summary>
    /// 输入的左下角坐标和右上角坐标需要是在网格坐标系中的
    /// 例如网格坐标系下的 (0, 0) 对应世界坐标系下的 (0.5, 0.5) * GridSize + basePos 位置
    /// 网格坐标系下的 (3,3) 对应世界坐标系下的 (2.5, 2.5) * GridSize + basePos 位置
    /// </summary>
    /// <param name="bottomleft">网格坐标系中的左下角坐标</param>
    /// <param name="upright">网格坐标系中的右上角坐标</param>
    public void AddRectEmptyPosToAllRegions(Vector2Int bottomleft, Vector2Int upright)
    {
        for (int i = 0; i < regionSetting.regions.Count; i++)
        {
            Vector3 basePos = Chessboard.Instance.transform.position;
            Vector3 tmpPos;
            
            Vector2Int bottomleftIntersectPart = new Vector2Int(Mathf.Max(regionSetting.regions[i].bottomleft.x, bottomleft.x),
                Mathf.Max(regionSetting.regions[i].bottomleft.y, bottomleft.y));
            Vector2Int uprightIntersectPart = new Vector2Int(Mathf.Min(regionSetting.regions[i].upright.x, upright.x),
                Mathf.Min(regionSetting.regions[i].upright.y, upright.y));
            
            for (int x = bottomleftIntersectPart.x; x <= uprightIntersectPart.x; x++)
            {
                for (int y = bottomleftIntersectPart.y; y <= uprightIntersectPart.y; y++)
                {
                    tmpPos = new Vector3(x + 0.5f, y + 0.5f, 0) * ChessboardImageController.Instance.GridSize + basePos;
       
                    if (!emptyPosInRegions[i].list.Contains(tmpPos))
                    {
                        emptyPosInRegions[i].list.Add(tmpPos);
                    }
                }
            }
        }
    }
    
    /// <summary>
    /// 判断输入的世界坐标系中的坐标是否对应某个区域中的空位
    /// </summary>
    /// <param name="pos">属于世界坐标系的坐标</param>
    /// <returns></returns>
    public bool IsEmptyPosInAnyRegions(Vector3 pos)
    {
        foreach (var emptyPosInRegion in emptyPosInRegions)
        {
            if (emptyPosInRegion.list.Contains(pos))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// 将输入的世界坐标系中的坐标,添加到所有能够包含这个坐标的区域的空位列表中
    /// </summary>
    /// <param name="pos">属于世界坐标系的坐标</param>
    public void AddEmptyPosToAllRegions(Vector3 pos)
    {
        for (int i = 0; i < regionSetting.regions.Count; i++)
        {
            Vector3 relativeGridPos = ChessPosConverter.WorldPosToRelativeGridPos(pos);

            if (regionSetting.regions[i].IsInRegion(relativeGridPos))
            {
                if (!emptyPosInRegions[i].list.Contains(pos))
                {
                    emptyPosInRegions[i].list.Add(pos);
                }
            }
        }
    }

    /// <summary>
    /// 将输入的世界坐标系中的坐标,从所有区域的空位列表中移除
    /// </summary>
    /// <param name="pos">属于世界坐标系的坐标</param>
    public void RemoveEmptyPosFromAllRegions(Vector3 pos)
    {
        for (int i = 0; i < regionSetting.regions.Count; i++)
        {
            emptyPosInRegions[i].list.Remove(pos);
        }
    }

    public int GetAvailableRegionIndex()
    {
        // Get available regions
        
        if (regionSetting.regions.Count == 0)
        {
            Debug.LogError("There is not an available region setting");
            return -1;
        }
        
        List<int> availableRegionIndexs = Enumerable.Range(0, regionSetting.regions.Count).ToList();

        for (int i = availableRegionIndexs.Count - 1; i >= 0; i--)
        {
            if (emptyPosInRegions[i].list.Count == 0)
            {
                availableRegionIndexs.RemoveAt(i);
            }
        }

        if (availableRegionIndexs.Count == 0)
        {
            Debug.LogError("There is not an available region with empty pos");
            return -1;
        }
        
        // Choose a regions from available regions randomly

        int randRegionIndex = Random.Range(0, availableRegionIndexs.Count);

        return availableRegionIndexs[randRegionIndex];
    }

    public void OnDrawGizmos()
    {
        Gizmos.color = new Color(0.3f, 0.3f, 1f, 0.5f);
        
        if(showEmptyPosInRegionsIndex >= 0 && showEmptyPosInRegionsIndex < regionSetting.regions.Count)
        {
            foreach (Vector3 emptyPos in emptyPosInRegions[showEmptyPosInRegionsIndex].list)
            {
                Gizmos.DrawCube(emptyPos, Vector3.one * ChessboardImageController.Instance.GridSize);
            }
        }
    }
}

[Serializable]
public class Positions
{
    public List<Vector3> list = new List<Vector3>();
}

使用示例:

public static GameObject TryRandomSpawn()
{
    // Choose a regions from available regions randomly

    int randRegionIndex = RegionsController.Instance.GetAvailableRegionIndex();

    if (randRegionIndex == -1)
        return null;

    if (RegionsController.Instance.emptyPosInRegions.Count <= randRegionIndex)
    {
        Debug.LogError("传入的有效区域序号不存在!请检查传入的 AvailableRegionIndex!");
        return null;
    }
    
    if (RegionsController.Instance.emptyPosInRegions[randRegionIndex].list.Count() == 0)
    {
        Debug.LogError("传入的有效区域中实际上没有空位!请检查传入的 AvailableRegionIndex!");
        return null;
    }

    // Choose a empty pos from selected region

    int randPosIndex = Random.Range(0, RegionsController.Instance.emptyPosInRegions[randRegionIndex].list.Count());
    Vector3 spawnPos = RegionsController.Instance.emptyPosInRegions[randRegionIndex].list[randPosIndex];
        
    // Spawn

    return Spawn(spawnPos);
}

维护示例:

如果一个棋子从 oldPos 移动到 newPos,那么

// 我怎么在棋盘中生成一个棋子?为了确保不出错,我是已经在棋盘中创建并且在维护一个空位列表,这样每次取空位只需要 o(1) 时间复杂度
// 但是麻烦的就是维护这个列表,需要注意
// 主要的逻辑是要注意调整棋盘中的空位列表

// 空位列表都是世界坐标

// 为什么是 ToAllRegions 因为现在的空位列表是一个区域一个空位列表
// 因为策划里面是先选区域再选区域里的空位,所以一个区域有一个空位列表
// 这样的话不同的区域的空位列表可能就会重合
// 所以在处理空位列表的时候要看一遍所有区域
RegionsController.Instance.AddEmptyPosToAllRegions(oldPos);
RegionsController.Instance.RemoveEmptyPosFromAllRegions(newPos);

如果棋盘的大小发生变化,那么需要添加两个拓展矩形的空位到所有区间的空位列表中

private void RefreshEmptyPosInRegions()
{
    Vector2Int gridPosUpRight = GetGridPosUpRight();

    Vector2Int rectBottomLeft1 = new Vector2Int(lastGridPosUpRight.x + 1, 0);
    Vector2Int rectBottomLeft2 = new Vector2Int(0, lastGridPosUpRight.y + 1);
    Vector2Int rectUpRight2 = new Vector2Int(lastGridPosUpRight.x, gridPosUpRight.y);
    
    RegionsController.Instance.AddRectEmptyPosToAllRegions(rectBottomLeft1, gridPosUpRight);
    RegionsController.Instance.AddRectEmptyPosToAllRegions(rectBottomLeft2, rectUpRight2);

    lastGridPosUpRight = gridPosUpRight;
}

这个方法的难点就在于维护……主要是这个空位列表的维护很容易出错

始终要注意棋子移动的时候有没有调用 AddEmptyPosToAllRegionsRemoveEmptyPosFromAllRegions,不然别人在取空位的时候,可能取到的空位上面已经有棋子了,就会发生棋子叠加棋子的情况