Unity即时战略/塔防项目实战(一)——构造网格建造系统

2023-12-12 03:40

本文主要是介绍Unity即时战略/塔防项目实战(一)——构造网格建造系统,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Unity即时战略/塔防项目实战(一)—— 构造网格建造系统

效果展示

Unity RTS游戏网格建造系统

实现原理

地形和格子划分,建造系统BuildManager构建

地形最终需要划分成一个一个的小方格,首先定义一下小方格:

private struct MapCellNode
{public float height;		// 格子的中心高度public float steepness;		// 格子的梯度public Building current;	// 格子中存储的建筑
}

将地图分成m*n的小个子,用一个二维数组容纳这些格子,并对这些格子进行初始化:

// 盛放格子的容器
private static MapCellNode[,] mapCells;// 初始化格子,并计算每个格子的高度和坡度
private void InitMapCells()
{var terrainData = _terrain.terrainData;int gridWidth = (int)(terrainData.bounds.size.x / cellSize.x);int gridHeight = (int)(terrainData.bounds.size.z / cellSize.y);mapCells = new MapCellNode[gridWidth, gridHeight];for (int i = 0; i < gridWidth; ++i){for (int j = 0; j < gridHeight; ++j){mapCells[i, j].current = null;var center = GetCellLocalPosition(i, j);mapCells[i, j].height = center.y;var steepness = terrainData.GetSteepness(center.x / terrainData.size.x, center.z/terrainData.size.z);mapCells[i, j].steepness = steepness;}}
}

定义建造系统的一些API方便在其他地方使用:

// 根据格子索引,获取格子中心点的本地坐标
public static Vector3 GetCellLocalPosition(int w, int h)
{Vector3 withoutHeight = new(w * cellSize.x + cellSize.x * 0.5f, 0, h * cellSize.y + cellSize.y * 0.5f);return GetTerrainPosByLocal(withoutHeight);
}// 根据格子索引,获取格子中心点的世界坐标
public static Vector3 GetCellWorldPosition(int w, int h)
{return Instance.transform.TransformPoint(GetCellLocalPosition(w, h));
}// 计算地图上的本地坐标点,所属网格的索引
public static (int, int) GetCellIndexByLocalPosition(Vector3 local)
{return ((int)(local.x / Instance._cellSize.x), (int)(local.z / Instance._cellSize.y));
}// 计算地图上的世界坐标点,所属网格的索引
public static (int, int) GetCellIndexByWorldPosition(Vector3 world)
{return GetCellIndexByLocalPosition(Instance.transform.InverseTransformPoint(world));
}// 根据给定的格子区域(起始格子索引、宽度和高度),计算区域内所有格子的平均高度
public static float GetGridAverageHeight(int sx, int sy, int w, int h)
{float height = 0;int count = 0;for (int x = sx; x < sx+w; ++x){if( x < 0 || x >= gridSize.x)continue;for (int y = sy; y < sy + h; ++y){if( y < 0 || y >= gridSize.y)continue;height += mapCells[x, y].height;++count;}}if (count > 0)return height / count;return 0;
}
PreBuilding 和“开始建造”

由于一次只能建造一个建筑,因此,当开始建造时,首先持有待建造的物体,用current来保存待建造的物体。

// 开始建造,根据id查询待建物,并持有它。
public static void TakeBuilding(string id)
{if (!Instance.preBuildings.TryGetValue(id, out PreBuilding pb))return;BeginBuild(pb);
}// 准备建造指定的建筑物
private static void BeginBuild(PreBuilding pb)
{// 让待建物准备建造(重置待建物的材质参数等)pb.BeginBuild();currentBuilding = pb;// 在待建物周围绘制方格线Instance.buildLineDrawer.gameObject.SetActive(true);Transform trans = Instance.buildLineDrawer.transform;trans.SetParent(currentBuilding.transform);trans.localPosition = projectorOffset - currentBuilding.AlignToCellOffset();// 如果待建物是具有攻击范围或影响范围的,则显示范围指示器并设置半径为待建物的影响范围if (currentBuilding.canAttack){Instance.attackCircel.gameObject.SetActive(true);Instance.attackCircel.SetRadius(currentBuilding.AttackRadius);trans = Instance.attackCircel.transform;trans.SetParent(currentBuilding.transform);trans.localPosition = projectorOffset;}
}

然后就是建造检测逻辑:

private void Update()
{// 不在建造状态就返回if (currentBuilding is null || currentBuilding.IsBuilding){
#if DEBUG_MODDisplayDebugInfo();
#endifreturn;}// 按下右键就取消建造if (Input.GetMouseButtonDown(1)){CancelBuild();return;}// 不在UI上才建造if (EventSystem.current.IsPointerOverGameObject()){if (!Cursor.visible)Cursor.visible = true;return;}// 获取建造点if (!Physics.Raycast(mainCamera.ScreenPointToRay(Input.mousePosition), out RaycastHit hit, 100f,groundLayer.value)){if (!Cursor.visible)Cursor.visible = true;return;}if (Cursor.visible)Cursor.visible = false;// 按下R键就旋转待建物(换个朝向)if (Input.GetKeyDown(KeyCode.R))currentBuilding.NextRotation();// 获取地图格子索引var (x, y) = GetCellIndexByWorldPosition(hit.point);// 尝试放入待建物,如无法放置返回falseif (currentBuilding.CheckBuildingIndexPosOnGrid(x, y)){// 按下左键,准备结束建造if (Input.GetMouseButtonDown(0)){PrepareEndBuild();}}
}

检测能否放置在当前位置的方法如下:

public bool CheckBuildingIndexPosOnGrid(int x, int y)
{bool canBuild = true;// 根据朝向计算当前占用格子的宽度和高度// 比如:一个建筑物南北朝向放置时占用3*2个格子,但是东西朝向放置时将占用2*3个格子。var (w, h) = GetRealSizeWithDir();int dx = (w - 1) / 2;int dy = (h - 1) / 2;int sx = x - dx;int sy = y - dy;// 获取所占格子的平均地形高度float aheight = BuildManager.GetGridAverageHeight(sx, sy, w, h);string info = "超出范围";for (int px = sx; canBuild && px < sx + w; ++ px){// 判定x方向是否超出地图边界if (px < 0 || px >= BuildManager.gridSize.x){canBuild = false;break;}for (int py = sy; py < sy + h; ++py){// 判定z方向是否超出地图边界if (py < 0 || py >= BuildManager.gridSize.y){canBuild = false;break;}// 判定所占用的格子上是否已经存在其他建筑if (BuildManager.GetBuildingWithCell(px, py) is not null){canBuild = false;info = "已存在其他建筑";break;}// 判定格子地形高度与平均高度是否相差太多if (Mathf.Abs(aheight - BuildManager.GetCellHeight(px, py)) > 0.2f){canBuild = false;info = "地形不平";break;}// 判定格子坡度是否太陡if (BuildManager.GetCellStepness(px, py) > 3f){canBuild = false;info = "坡度太陡";break;}}}// 根据格子索引,获取世界坐标,并将其对齐到网格// AlignToCellOffset意义为:假设待建物体的中心点在物体的几何中心,那么,如果所占格子尺寸为奇数,// 则建筑是对称的,偏移为0;如果所占格子尺寸为偶数,则该建筑不是对称的,需要偏移半个单元格。var pos = BuildManager.GetCellWorldPosition(x, y) + AlignToCellOffset();// 如果能够在此处建造,则设置索引,并设置待建物材质为“绿色”,否则设置为“红色”。if (canBuild){_indexPos.x = sx;_indexPos.y = sy;_indexPos.width = w;_indexPos.height = h;preMaterial.SetColor(CommDefine.PrebuildColor, BuildManager.preBuildNormalColor);                }else{preMaterial.SetColor(CommDefine.PrebuildColor, BuildManager.preBuildBadColor);InfoTips.Display(info, pos, 1.2f );}// 设置待建物的世界坐标transform.position = pos;return canBuild;
}

当按下鼠标,确定在此处建造时:

// 准备完成建造
private static void PrepareEndBuild()
{// 恢复鼠标显示if (!Cursor.visible)Cursor.visible = true;// 关闭网格显示、关闭范围指示,开始播放建造动画currentBuilding.EndBuild();Instance.buildLineDrawer.gameObject.SetActive(false);if(currentBuilding.canAttack)Instance.attackCircel.gameObject.SetActive(false);
}// 建造完成(建造动画播放完成)
private static void FinishBuild()
{// 实例化真正要建造的物体Building bd = currentBuilding.CreateBuilding();// 将建筑保存到网格中SaveCurrentBuilding(bd);// 置空currentcurrentBuilding = null;
}
网格及范围指示的绘制

因为地形是不平的,要在不平整的地面上完美的绘制网格和范围指示器,那用到了投影(贴花),然后投影材质使用了自己写的shader,很简单:

  • 网格的Shader:
fixed4 frag(const v2f i) : SV_Target
{const float temp_output_2_0_g3 = 1 - _Width;const float2 appendResult10_g4 = float2(temp_output_2_0_g3, temp_output_2_0_g3);const float2 temp_output_11_0_g4 = abs(frac(i.uv0 * _ScaleOffset.xy + _ScaleOffset.zw) * 2.0 + -1.0) -appendResult10_g4;const float2 break16_g4 = 1.0 - temp_output_11_0_g4 / fwidth(temp_output_11_0_g4);float4 res = 1 - saturate(min(break16_g4.x, break16_g4.y)).xxxx;const float len = length(i.uv0 - float2(0.5,0.5));res *= step(len, 0.5);res *= smoothstep( 1-len, _min, _max);return res * _Color;
}
  • 范围指示的Shader:
fixed4 frag(const v2f i) : SV_Target
{const float radius = _Radius * 0.5;const float width = _Width * 0.5;const float len = length(i.uv0 - float2(0.5,0.5));float4 res = step(len, radius);const float4 inner = step(len, radius - width);res -= inner;res *= _Color;return res;
}
建造过程动画

由于缺乏美术资源,建造过程通过一个融合动画来展示建造过程,融合用ASE插件做的Shader:
在这里插入图片描述

源码下载:

猛击此处下载

这篇关于Unity即时战略/塔防项目实战(一)——构造网格建造系统的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/483124

相关文章

IDEA运行spring项目时,控制台未出现的解决方案

《IDEA运行spring项目时,控制台未出现的解决方案》文章总结了在使用IDEA运行代码时,控制台未出现的问题和解决方案,问题可能是由于点击图标或重启IDEA后控制台仍未显示,解决方案提供了解决方法... 目录问题分析解决方案总结问题js使用IDEA,点击运行按钮,运行结束,但控制台未出现http://

解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题

《解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题》文章详细描述了在使用lombok的@Data注解标注实体类时遇到编译无误但运行时报错的问题,分析... 目录问题分析问题解决方案步骤一步骤二步骤三总结问题使用lombok注解@Data标注实体类,编译时

C语言小项目实战之通讯录功能

《C语言小项目实战之通讯录功能》:本文主要介绍如何设计和实现一个简单的通讯录管理系统,包括联系人信息的存储、增加、删除、查找、修改和排序等功能,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录功能介绍:添加联系人模块显示联系人模块删除联系人模块查找联系人模块修改联系人模块排序联系人模块源代码如下

SpringBoot项目中Maven剔除无用Jar引用的最佳实践

《SpringBoot项目中Maven剔除无用Jar引用的最佳实践》在SpringBoot项目开发中,Maven是最常用的构建工具之一,通过Maven,我们可以轻松地管理项目所需的依赖,而,... 目录1、引言2、Maven 依赖管理的基础概念2.1 什么是 Maven 依赖2.2 Maven 的依赖传递机

C#实现系统信息监控与获取功能

《C#实现系统信息监控与获取功能》在C#开发的众多应用场景中,获取系统信息以及监控用户操作有着广泛的用途,比如在系统性能优化工具中,需要实时读取CPU、GPU资源信息,本文将详细介绍如何使用C#来实现... 目录前言一、C# 监控键盘1. 原理与实现思路2. 代码实现二、读取 CPU、GPU 资源信息1.

Vue项目中Element UI组件未注册的问题原因及解决方法

《Vue项目中ElementUI组件未注册的问题原因及解决方法》在Vue项目中使用ElementUI组件库时,开发者可能会遇到一些常见问题,例如组件未正确注册导致的警告或错误,本文将详细探讨这些问题... 目录引言一、问题背景1.1 错误信息分析1.2 问题原因二、解决方法2.1 全局引入 Element

在C#中获取端口号与系统信息的高效实践

《在C#中获取端口号与系统信息的高效实践》在现代软件开发中,尤其是系统管理、运维、监控和性能优化等场景中,了解计算机硬件和网络的状态至关重要,C#作为一种广泛应用的编程语言,提供了丰富的API来帮助开... 目录引言1. 获取端口号信息1.1 获取活动的 TCP 和 UDP 连接说明:应用场景:2. 获取硬

Golang操作DuckDB实战案例分享

《Golang操作DuckDB实战案例分享》DuckDB是一个嵌入式SQL数据库引擎,它与众所周知的SQLite非常相似,但它是为olap风格的工作负载设计的,DuckDB支持各种数据类型和SQL特性... 目录DuckDB的主要优点环境准备初始化表和数据查询单行或多行错误处理和事务完整代码最后总结Duck

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

2.1/5.1和7.1声道系统有什么区别? 音频声道的专业知识科普

《2.1/5.1和7.1声道系统有什么区别?音频声道的专业知识科普》当设置环绕声系统时,会遇到2.1、5.1、7.1、7.1.2、9.1等数字,当一遍又一遍地看到它们时,可能想知道它们是什... 想要把智能电视自带的音响升级成专业级的家庭影院系统吗?那么你将面临一个重要的选择——使用 2.1、5.1 还是