zt3ff3n 发表于 2022-11-26 11:26

战棋游戏开发入门(一)

寒假开始时候做的一个游戏,仿火纹的战棋游戏。现在的进度在完成了战斗处理就完成基本框架了。写个教程给大家,也算是整理一下写的东西。
食用本教程你需要有:

[*]       掌握C#以及unity基本技能
[*]       基础的数据结构
[*]       基本的算法知识
大部分都会讲的,特别基础的部分会一笔带过。第一次写教程,有问题都可以提出。
<hr/>一 :地图

       在做任何事之前一定要规划好,我现在特别后悔当初没有写导图,现在看来结构有些问题,但既然写了就凑合着用用,吸取下自己的教训。
       战旗游戏,最基本的元素是什么地图,任何元素都是在地图上实现的。这里地图实现我用的tilemap实现。tilemap 瓷砖地图,顾名思义就是把图片像砖块一样拼贴在一起。很多2D游戏都会用到。tilemap详细教程浏览B站视频,这里只讲到工程需要的。
       导入tilemap后,我们先右键“层级”面板 ->“3D对象”->“瓦片”即可创建出基本的tilemap。



Grid下有网格组件,控制子对象网格大小。而tilemap才是实现图片的对象

       我们再创建个"MapManager"的脚本,放到Grid内,该脚本就是我们控制地图生成,显示的组件。
      tilemap并不能直接显示sprit,而是需要tile,所以我们这里创建一些tile,以及图块。(图片请自备),需要的图片大致分类土地,山地,小河,海,房屋,墙壁等。



这是我自制的图集,在Sprite Editor拆封。土地部分实际上只需要无边界的一块即可。

      这个只是最基本的图块。如图4,5,6行。分别是陆地,小河,墙壁。可以看到它们连接的边数是不同的,这样就能做出任意的轨迹。但tile只能使用一个图片,我们可以通过脚本去控制图片的类型,但还是有点麻烦。这里就需要官方出的2Dextras插件,在GitHub上百度应该能找到。(一直不理解官方为什么不把这个插件加进去)
      安装后,用项目右键创建最上端就是瓦片,选择rule tile。这么多tile都是继承TileBase这个类写的脚本,之后我也会教大家写一个自定义tile。


这是我以及创建好的rule map。九宫格是tile的生成规则。默认sprite为不符合下面任意一个规则显示的图片。九宫格箭头的含义是该tile指向的位置有相同的tile,X则为没有,空则无所谓。同时将rule改为已旋转即可实现图片旋转。
接着让我们了解下tilemap的生成图块的API
BoxFill(Vector3Int position, TileBase tile, int startX, int startY, int endX, int endY);

//position用于确定原点,tile即图块类型,startX/Y开始坐标 endX/Y结束坐标

public void SetTiles(Vector3Int[] positionArray, TileBase[] tileArray);

//两个数组分别对应坐标和图块
这里我们就需要读取tilemap,这里我用的是官方最新出的AddressableAssets。相关教程可观看unity官方达哥出的教程。
安装完成后,打开窗口,资源管理,Addressable,group。


拖动所有tile到group上,修改左边的名次,点击任意label,manage label,添加你喜欢的名字。将所有tile添加上该label。完成。


让我们重新回到mapmanager脚本中。显然为了显示地图我们需要一个二维数组,以及地图数据,建立一个节点类,地图类,其实这里应该用struct的。但是应该寻路的需要只能用class,现在想来应该把两个分开的。有能力可以自行改造。
public class Node
{
    public Vector3Int pos;//位置信息
    public Node parent;//父节点,寻路用(就是这个)
    public int elemType;//地图类型
    public int action = -1;//行动力,寻路用
    public UnitBase unit; //该图块的单位
    public Node(Vector3Int pos)
    {
      this.pos = pos;
    }

    public Node(Vector3Int pos, Node parent) : this(pos)
    {
      this.parent = parent;
    }

    public Node(Vector3Int pos, int elemType) : this(pos)
    {
      this.elemType = elemType;
    }
}

//未完成
public struct MapData
{
    public int width;//地图宽度
    public int high;//地图长度
    public mapType mapType;//enum 地图类型,主要用于不同场景,地图显示参考的是战纹的地图
    public int[,] elemTypes;//地图元素
    public BuildBase[] builds;//建筑基类,暂时没用

    public MapData(int width, int high, mapType mapType, int[,] elemTypes , BuildBase[] builds)
    {
      this.width = width;
      this.high = high;
      this.mapType = mapType;
      this.elemTypes = elemTypes;
      this.builds = builds;
    }
}

public enum elemType { soil, road, river, mountain , tree , wall , bridge ,house}mapmanger先声明这些:
    public Tilemap[] tilemaps;//tilemap层
    public IList<TileBase> elemTile;
    public Node[,] mapNodes;//节点信息我们来写地图显示方法,其实你也可以直接写死在sence里,那么这部分都可以不用看了。
public void Initialition(MapData mapData)这里接收一个mapdata来初始化,我想的是由一个GameDataManager来分发游戏数据。
AssetLabelReference asset=new AssetLabelReference();//标签资源
asset.labelString = mapData.mapType.ToString();//这里把标签名设置为地图类型,
                               用于读取同一图集的tile。好吧,label不是乱写的
Addressables.LoadResourceLocationsAsync(asset).Completed += MapManager_Completed;
//这东西不支持await,所以Completed事件,读取完成调用MapManager_Completed
//这个方法并不是读取资源,而是观察资源,下面的方法创建了一个映射,来显示正确的显示顺序
void MapManager_Completed(UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<IList<UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation>> obj)
{
    var name = obj.Result;这里类型是ilist<>
    assetsMapping = new int;//assetsMapping是外面声明的int[]
    for (int i = 0; i < name.Count; i++)
    {
         elemType e = (elemType)Enum.Parse(typeof(elemType), name.PrimaryKey, true);
         assetsMapping[(int)e] = i;
   }
   Addressables.LoadAssetsAsync<TileBase>(asset, null).Completed += LoadedElem;
   //这个才是真正读取tile的方法。
}
      void LoadedElem(UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<IList<TileBase>> obj)
      {
            elemTile =obj.Result;

            List<Vector3Int> positions = new List<Vector3Int>();//坐标
            List<TileBase> tiles = new List<TileBase>();//该坐标的tile
            mapNodes = new Node;//声明节点大小

            for (int x = 0; x < mapData.width; x++)
            {
                for (int y = 0; y < mapData.high; y++)
                {
                  mapNodes = new Node(new Vector3Int(x, y, 0), mapData.elemTypes);
                  初始化每个节点

                  if (mapData.elemTypes != 0)
                  {
                     当节点类型不为0时,添加坐标和tile
                        positions.Add(new Vector3Int(x, y, 0));
                        tiles.Add(elemTile]]);
                  }
                }
            }

            for (int i = 0; i < tilemaps.Length; i++)初始化各图层大小,原点
            {
                tilemaps.origin = Vector3Int.zero;
                tilemaps.size = new Vector3Int(mapData.width, mapData.high, 0);
             }
            for (int i = 0; i < mapData.builds.Length; i++)//将建筑信息写入unit,这里有个BUG,人物和建筑无法重叠
            {
                var p = mapData.builds.Pos;
                mapNodes.unit = mapData.builds;
            }
            0图层填充土地,1图层填充其他的tile
            tilemaps.BoxFill(Vector3Int.zero, elemTile], 0, 0, mapData.width, mapData.high );
            tilemaps.SetTiles(positions.ToArray(), tiles.ToArray());

            if(Initialed != null)
            Initialed.Invoke();//地图生成完的事件
      }运行应该就能看到地图显示了。

本章教程到此结束,如果有任何问题,请提出来。也可以关注的我推特 @wozpren2

c0d3n4m 发表于 2022-11-26 11:35

非常感谢,收藏关注了

DomDomm 发表于 2022-11-26 11:42

纠正一点,是【战棋游戏】,不是【战旗游戏】。
页: [1]
查看完整版本: 战棋游戏开发入门(一)