十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

UE5 C++ 实战:从零构建 CRPG 战斗网格系统

UE5 C++ 实战:从零构建 CRPG 战斗网格系统 这次我们来看一个面向 UE5 C 开发者的实战教程主题是“从零开始做 CRPG 战斗系统中的网格”。对于想要深入理解 UE5 底层机制特别是构建策略、战棋类游戏核心玩法的开发者来说网格系统是绕不开的基石。它直接决定了角色的移动范围、技能施放区域、地形阻挡和战斗交互的逻辑边界。本文的核心不是空谈理论而是聚焦于如何用 C 在 UE5 中从零搭建一个可运行、可扩展的战斗网格系统。我们将重点关注其数据结构设计、与游戏世界的交互、以及如何为后续的寻路、范围检测等功能打下坚实基础。无论你是想制作一款经典的 CRPG还是希望在动作游戏中加入策略元素这套网格系统的实现思路都极具参考价值。下面我们将从网格系统的核心概念与设计目标开始逐步深入到 C 类的实现、网格数据的生成与管理并最终在游戏世界中可视化呈现。整个过程会附带详细的代码示例和操作步骤确保你可以跟着动手实现。1. 核心能力速览在深入代码之前我们先快速了解这个战斗网格系统要解决的核心问题及其关键特性。能力项说明项目类型UE5 C 游戏子系统开发教程技术栈Unreal Engine 5, C, 游戏框架编程核心功能创建并管理一个基于网格的战斗场景支持网格数据的存储、查询与可视化为移动、攻击范围等规则提供底层支持。推荐硬件满足 UE5 开发需求的 PC建议 16GB 内存支持 DX12 的显卡。显存占用主要取决于场景复杂度和网格密度初期测试对显卡要求不高。启动/运行方式在 UE5 编辑器中创建 C 项目编写并编译代码在游戏运行时或编辑器视口中查看网格。是否支持“API”/接口是。通过自定义的 C 类提供公共方法用于获取网格信息、判断格子状态等可供蓝图或其他 C 系统调用。是否支持“批量”任务网格系统本身即用于管理批量“格子”单元。支持遍历所有格子、批量设置格子属性如是否可通行、地形代价。适合场景战棋SRPG、策略角色扮演CRPG、回合制战术游戏的战斗场景开发任何需要离散化空间进行逻辑判断的游戏玩法。2. 适用场景与使用边界这个网格系统是为特定类型的游戏玩法服务的理解其适用边界能帮助你更好地决定是否采用以及如何扩展。适合谁UE5 C 初学者到进阶者希望通过一个完整的子系统实践来学习 UE 的 C 编程范式、Gameplay 框架和调试方法。策略/战棋游戏开发者需要为角色移动、技能范围、战场遮挡等玩法建立可靠的空间数据模型。对性能敏感的原型开发者相比纯蓝图C 实现的网格逻辑在计算密集的寻路或范围搜索时更有性能优势也便于优化。能解决什么问题空间离散化将连续的 3D 游戏世界划分为规则的网格使复杂的空间关系判断如“距离3格以内”简化为数组索引操作。游戏逻辑载体每个网格可以存储丰富的信息如通行成本、地形类型、占据单位、特效标记等成为所有战斗规则移动、攻击、技能的运算基础。可视化与调试在开发阶段将网格绘制出来可以直观验证逻辑是否正确快速定位寻路失败、范围计算错误等问题。不适合什么场景需要极高动态性的场景如果地形在战斗中频繁、大规模地改变如大量可破坏物体每帧更新网格的成本可能较高需要考虑动态网格或更高级的空间划分结构如导航网格 NavMesh 的实时更新。纯动作或FPS游戏这类游戏通常依赖连续的物理模拟和射线检测离散网格可能不是最高效的选择。超大规模开放世界一个覆盖整个开放世界的均匀网格会非常庞大且稀疏。通常做法是采用局部网格如围绕玩家生成或分层空间数据结构。合规与设计边界提醒本教程实现的是游戏逻辑层面的网格与渲染网格Mesh无关不涉及模型修改或材质侵权问题。网格数据属于游戏设计核心资产确保其生成规则和数值平衡符合游戏整体设计目标。3. 环境准备与前置条件在开始编码前请确保你的开发环境已就绪。操作系统Windows 10/11 64位 或 macOS需注意 UE5 对 Apple Silicon 的支持情况。Unreal Engine 5建议使用 5.3 或更新的稳定版本。确保已通过 Epic Games Launcher 或源码编译安装。Visual Studio 2022Windows 上的首选 IDE。安装时务必勾选“使用 C 的游戏开发”工作负载。对于 Mac 用户可使用 Xcode。项目创建启动 UE5 编辑器选择“游戏”类别然后选择“空白”模板。项目名称可定为CRPGBattleGrid确保“使用 C”选项被勾选然后点击创建。项目路径不要包含中文或特殊字符。基础认知具备基础的 C 语法知识类、对象、STL 容器如TArray。对 UE5 的基本概念有了解如UObject,AActor,FVector等。知道如何在编辑器中创建新的 C 类。4. 网格系统设计与核心类创建我们将创建两个核心 C 类UBattleGrid和ABattleGridVisualizer。前者负责数据逻辑后者负责在世界中显示。4.1 创建网格数据类 (UBattleGrid)首先创建一个管理网格数据的组件或子系统。这里我们创建一个继承自UObject的类方便被其他对象持有。在编辑器内容浏览器中右键点击“C类”文件夹选择“新建C类”。在“选择父类”中搜索并选择“Object”即UObject点击“下一步”。类名填写BattleGrid路径保持默认或按需修改点击“创建”。Visual Studio 会自动打开新生成的文件。我们首先在BattleGrid.h中定义网格的核心数据结构和接口。// BattleGrid.h #pragma once #include CoreMinimal.h #include UObject/NoExportTypes.h #include BattleGrid.generated.h // 前向声明用于格子状态枚举如果需要 UENUM(BlueprintType) enum class EGridTileState : uint8 { Empty UMETA(DisplayName 空), Occupied UMETA(DisplayName 被占据), Blocked UMETA(DisplayName 阻塞), Difficult UMETA(DisplayName 困难地形), }; // 单个格子的数据结构 USTRUCT(BlueprintType) struct FGridTile { GENERATED_BODY() public: // 格子在世界中的中心位置 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Tile) FVector WorldLocation; // 格子的索引 (X, Y) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Tile) FIntPoint GridIndex; // 格子状态 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Tile) EGridTileState State EGridTileState::Empty; // 移动到此格子的额外代价用于寻路 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Tile) float MovementCost 1.0f; // 是否可通行综合判断 bool IsWalkable() const { return State ! EGridTileState::Blocked; } FGridTile() {} FGridTile(const FVector InLocation, const FIntPoint InIndex) : WorldLocation(InLocation), GridIndex(InIndex) {} }; /** * 战斗网格数据管理类 */ UCLASS(Blueprintable, BlueprintType) class CRPGBATTLEGRID_API UBattleGrid : public UObject { GENERATED_BODY() public: UBattleGrid(); // 初始化网格指定左下角起点、格子大小、行列数 UFUNCTION(BlueprintCallable, Category Battle Grid) void InitializeGrid(const FVector GridOrigin, float TileSize, int32 InGridWidth, int32 InGridHeight); // 根据世界坐标获取对应的格子索引 UFUNCTION(BlueprintCallable, Category Battle Grid) bool GetTileIndexFromWorldLocation(const FVector WorldLocation, FIntPoint OutTileIndex) const; // 根据格子索引获取格子数据 UFUNCTION(BlueprintCallable, Category Battle Grid) FGridTile GetTile(const FIntPoint TileIndex) const; // 设置格子状态 UFUNCTION(BlueprintCallable, Category Battle Grid) void SetTileState(const FIntPoint TileIndex, EGridTileState NewState); // 获取网格宽度和高度 UFUNCTION(BlueprintPure, Category Battle Grid) int32 GetGridWidth() const { return GridWidth; } UFUNCTION(BlueprintPure, Category Battle Grid) int32 GetGridHeight() const { return GridHeight; } // 获取所有格子只读用于遍历 const TArrayFGridTile GetAllTiles() const { return Tiles; } protected: // 网格左下角起点世界坐标 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Config) FVector Origin; // 每个格子的边长正方形 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Config, meta (ClampMin 10.0)) float TileSize 100.0f; // 网格宽度列数 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Config, meta (ClampMin 1)) int32 GridWidth 10; // 网格高度行数 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Config, meta (ClampMin 1)) int32 GridHeight 10; private: // 存储所有格子的数组 UPROPERTY() TArrayFGridTile Tiles; // 内部方法根据索引计算世界坐标 FVector GetWorldLocationFromTileIndex(const FIntPoint Index) const; };4.2 实现网格数据类接下来在BattleGrid.cpp中实现这些功能。// BattleGrid.cpp #include BattleGrid.h UBattleGrid::UBattleGrid() { // 构造函数中可以设置默认值 Origin FVector::ZeroVector; TileSize 100.0f; GridWidth 10; GridHeight 10; } void UBattleGrid::InitializeGrid(const FVector GridOrigin, float InTileSize, int32 InGridWidth, int32 InGridHeight) { Origin GridOrigin; TileSize InTileSize; GridWidth InGridWidth; GridHeight InGridHeight; Tiles.Empty(); Tiles.Reserve(GridWidth * GridHeight); for (int32 Y 0; Y GridHeight; Y) { for (int32 X 0; X GridWidth; X) { FIntPoint Index(X, Y); FVector TileWorldLocation GetWorldLocationFromTileIndex(Index); Tiles.Add(FGridTile(TileWorldLocation, Index)); } } UE_LOG(LogTemp, Log, TEXT(Battle Grid Initialized. Tiles: %d), Tiles.Num()); } FVector UBattleGrid::GetWorldLocationFromTileIndex(const FIntPoint Index) const { // 计算格子中心点的世界坐标 // 假设X轴对应网格的“右”方向Y轴对应网格的“前”方向 float X Origin.X (Index.X 0.5f) * TileSize; float Y Origin.Y (Index.Y 0.5f) * TileSize; float Z Origin.Z; // 暂时假设网格在水平面上Z值不变 return FVector(X, Y, Z); } bool UBattleGrid::GetTileIndexFromWorldLocation(const FVector WorldLocation, FIntPoint OutTileIndex) const { if (Tiles.Num() 0) { return false; } // 计算相对于原点的偏移量 float DeltaX WorldLocation.X - Origin.X; float DeltaY WorldLocation.Y - Origin.Y; // 转换为格子索引向下取整 int32 XIndex FMath::FloorToInt(DeltaX / TileSize); int32 YIndex FMath::FloorToInt(DeltaY / TileSize); // 检查索引是否在有效范围内 if (XIndex 0 XIndex GridWidth YIndex 0 YIndex GridHeight) { OutTileIndex FIntPoint(XIndex, YIndex); return true; } return false; } FGridTile UBattleGrid::GetTile(const FIntPoint TileIndex) const { int32 ArrayIndex TileIndex.Y * GridWidth TileIndex.X; if (Tiles.IsValidIndex(ArrayIndex)) { return Tiles[ArrayIndex]; } // 返回一个无效的格子或抛出错误这里简单返回一个默认构造的 UE_LOG(LogTemp, Warning, TEXT(Invalid tile index requested: (%d, %d)), TileIndex.X, TileIndex.Y); return FGridTile(); } void UBattleGrid::SetTileState(const FIntPoint TileIndex, EGridTileState NewState) { int32 ArrayIndex TileIndex.Y * GridWidth TileIndex.X; if (Tiles.IsValidIndex(ArrayIndex)) { Tiles[ArrayIndex].State NewState; } }4.3 创建网格可视化器 (ABattleGridVisualizer)数据有了我们需要在游戏世界中看到它。创建一个AActor来负责绘制网格。同样方式创建一个继承自Actor的 C 类命名为BattleGridVisualizer。在头文件中我们引入BattleGrid并添加绘制组件。// BattleGridVisualizer.h #pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include BattleGrid.h // 引入网格类 #include BattleGridVisualizer.generated.h UCLASS() class CRPGBATTLEGRID_API ABattleGridVisualizer : public AActor { GENERATED_BODY() public: ABattleGridVisualizer(); protected: virtual void BeginPlay() override; virtual void Tick(float DeltaTime) override; virtual void OnConstruction(const FTransform Transform) override; public: // 用于调试绘制 virtual bool ShouldTickIfViewportsOnly() const override { return true; } // 属性关联的网格数据资产可以是一个数据资产实例 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Visualization) UBattleGrid* BattleGridData; // 属性是否在编辑器中始终显示网格 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Visualization) bool bAlwaysShowGrid true; // 属性网格线颜色 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Visualization) FColor GridLineColor FColor::Green; // 属性网格线厚度 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Grid Visualization, meta (ClampMin 0.1, ClampMax 5.0)) float GridLineThickness 1.0f; // 重新绘制网格例如数据更新后调用 UFUNCTION(BlueprintCallable, Category Grid Visualization) void RefreshGridVisualization(); private: void DrawDebugGrid(); };在源文件中实现绘制逻辑。我们使用 UE 的调试绘制功能DrawDebugBox和DrawDebugLine。// BattleGridVisualizer.cpp #include BattleGridVisualizer.h #include Engine/Engine.h #include DrawDebugHelpers.h // 必须包含这个头文件 ABattleGridVisualizer::ABattleGridVisualizer() { PrimaryActorTick.bCanEverTick true; PrimaryActorTick.bStartWithTickEnabled true; } void ABattleGridVisualizer::BeginPlay() { Super::BeginPlay(); // 如果网格数据为空可以在这里尝试创建或加载默认数据 if (!BattleGridData) { UE_LOG(LogTemp, Warning, TEXT(BattleGridVisualizer has no BattleGridData assigned!)); } } void ABattleGridVisualizer::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 每帧都绘制或者根据条件绘制 if (bAlwaysShowGrid BattleGridData) { DrawDebugGrid(); } } void ABattleGridVisualizer::OnConstruction(const FTransform Transform) { Super::OnConstruction(Transform); // 在编辑器中进行属性更改时可以触发重绘通过刷新 RefreshGridVisualization(); } void ABattleGridVisualizer::RefreshGridVisualization() { // 这个函数主要用于蓝图调用或响应数据变化。 // 由于我们在Tick中绘制这里可以清空旧的绘制或设置一个重绘标志。 // 对于简单的调试绘制Tick中的实时绘制已足够。 } void ABattleGridVisualizer::DrawDebugGrid() { if (!BattleGridData || !GetWorld()) { return; } const TArrayFGridTile Tiles BattleGridData-GetAllTiles(); float HalfTileSize BattleGridData-TileSize * 0.5f; FVector BoxExtent FVector(HalfTileSize, HalfTileSize, 5.0f); // Z轴高度设小一点表示一个平面 for (const FGridTile Tile : Tiles) { // 根据格子状态选择颜色 FColor DrawColor GridLineColor; switch (Tile.State) { case EGridTileState::Blocked: DrawColor FColor::Red; break; case EGridTileState::Occupied: DrawColor FColor::Yellow; break; case EGridTileState::Difficult: DrawColor FColor::Orange; break; case EGridTileState::Empty: DrawColor GridLineColor; break; } // 绘制一个扁平的盒子代表格子 DrawDebugBox( GetWorld(), Tile.WorldLocation, BoxExtent, FColor::Transparent, // 填充色透明 false, // 不持久化 -1.0f, // 生命周期负数为单帧 0, // 深度优先级 GridLineThickness // 边框线厚度 ); // 也可以绘制交叉线或点来更清晰地表示格子中心 // DrawDebugPoint(GetWorld(), Tile.WorldLocation, 5.0f, DrawColor, false, -1.0f, 0); } // 绘制网格外边框可选 FVector Origin BattleGridData-Origin; float TotalWidth BattleGridData-GridWidth * BattleGridData-TileSize; float TotalHeight BattleGridData-GridHeight * BattleGridData-TileSize; FVector P0 Origin; FVector P1 Origin FVector(TotalWidth, 0, 0); FVector P2 Origin FVector(TotalWidth, TotalHeight, 0); FVector P3 Origin FVector(0, TotalHeight, 0); DrawDebugLine(GetWorld(), P0, P1, FColor::White, false, -1.0f, 0, GridLineThickness); DrawDebugLine(GetWorld(), P1, P2, FColor::White, false, -1.0f, 0, GridLineThickness); DrawDebugLine(GetWorld(), P2, P3, FColor::White, false, -1.0f, 0, GridLineThickness); DrawDebugLine(GetWorld(), P3, P0, FColor::White, false, -1.0f, 0, GridLineThickness); }5. 在编辑器中配置与测试代码编写完成后需要编译并在编辑器中测试。编译项目在 Visual Studio 中选择“生成 - 生成解决方案”。确保没有编译错误。回到 UE5 编辑器编译成功后UE5 编辑器会重新加载模块。创建并配置网格数据资产在内容浏览器中右键选择“蓝图类”-“所有类”-搜索“BattleGrid”我们的UBattleGrid类。创建一个基于BattleGrid的蓝图类命名为BP_BattleGridData。双击打开你可以看到Origin,TileSize,GridWidth,GridHeight等可编辑属性。设置一个合适的值例如原点为 (0,0,0)格子大小为200网格为8x8。保存这个蓝图资产。将可视化器放入场景在内容浏览器中找到BattleGridVisualizerC 类可能需要重启编辑器或在“C类”文件夹中查找。将其拖拽到场景视口中。在细节面板中找到Battle Grid Data属性点击下拉框选择我们刚才创建的BP_BattleGridData。确保bAlwaysShowGrid被勾选。运行测试点击编辑器工具栏上的“运行”按钮。在游戏窗口中你应该能看到绿色的网格线或你设置的颜色出现在场景中。尝试在BP_BattleGridData中修改GridWidth或TileSize停止运行后再次运行观察网格的变化。功能验证点网格生成确认网格线在预期的位置和数量出现。数据关联修改BP_BattleGridData的属性网格可视化应随之改变。坐标转换你可以在游戏运行时通过控制台命令或临时代码打印鼠标点击位置对应的格子索引验证GetTileIndexFromWorldLocation函数是否正确。6. 扩展功能网格交互与查询基础网格显示完成后我们可以为其添加一些实用的交互和查询功能使其真正成为战斗系统的一部分。6.1 在蓝图中测试坐标转换为了验证网格逻辑我们创建一个简单的测试蓝图。创建一个新的蓝图类继承自Actor命名为BP_GridTester。添加一个UBattleGrid类型的变量引用我们之前创建的BP_BattleGridData。在事件图表中添加以下逻辑Event Tick- 获取玩家控制器 - 获取鼠标光标在世界中的位置使用Deproject Mouse Position to World。将获取到的世界位置传递给BattleGrid变量的Get Tile Index From World Location函数。如果返回成功将格子索引FIntPoint转换为文本并使用Draw Debug String显示在屏幕上。通过移动鼠标你可以在屏幕上实时看到鼠标所在位置的网格索引这验证了坐标转换函数的正确性。6.2 实现格子状态查询与设置我们已经有了SetTileState函数。可以进一步扩展例如射线检测设置阻塞从玩家视角发射射线击中地面后获取击中点的格子索引并将其状态设置为Blocked。在可视化器中该格子会变成红色。单位占据当游戏中的角色移动到某个格子时调用SetTileState(TileIndex, EGridTileState::Occupied)。可视化器将其显示为黄色。// 示例在某个角色移动结束时调用的函数 void AMyCharacter::OnMovementFinished(const FVector NewLocation) { if (MyBattleGrid) // 假设角色持有一个网格引用 { FIntPoint TileIndex; if (MyBattleGrid-GetTileIndexFromWorldLocation(NewLocation, TileIndex)) { // 清除旧格子的占据状态如果有记录 if (LastOccupiedTile.IsValid()) { MyBattleGrid-SetTileState(LastOccupiedTile, EGridTileState::Empty); } // 设置新格子的占据状态 MyBattleGrid-SetTileState(TileIndex, EGridTileState::Occupied); LastOccupiedTile TileIndex; } } }6.3 实现移动范围计算广度优先搜索 BFS这是战斗系统的核心功能之一。给定一个起点和移动力计算出所有可到达的格子。在UBattleGrid类中添加一个新函数// BattleGrid.h UFUNCTION(BlueprintCallable, Category Battle Grid|Pathfinding) TArrayFIntPoint CalculateMovementRange(const FIntPoint StartTile, int32 MovementPoints); // BattleGrid.cpp TArrayFIntPoint UBattleGrid::CalculateMovementRange(const FIntPoint StartTile, int32 MovementPoints) { TArrayFIntPoint ReachableTiles; if (!Tiles.IsValidIndex(StartTile.Y * GridWidth StartTile.X)) { return ReachableTiles; } // 使用队列进行广度优先搜索 TQueueFIntPoint TileQueue; TMapFIntPoint, int32 CostSoFar; // 记录到达每个格子的最小代价 TileQueue.Enqueue(StartTile); CostSoFar.Add(StartTile, 0); // 四个方向的偏移量四连通网格 TArrayFIntPoint Directions { FIntPoint(1,0), FIntPoint(-1,0), FIntPoint(0,1), FIntPoint(0,-1) }; while (!TileQueue.IsEmpty()) { FIntPoint CurrentTile; TileQueue.Dequeue(CurrentTile); int32 CurrentCost CostSoFar[CurrentTile]; for (const FIntPoint Dir : Directions) { FIntPoint NextTile CurrentTile Dir; // 检查边界 if (NextTile.X 0 || NextTile.X GridWidth || NextTile.Y 0 || NextTile.Y GridHeight) { continue; } FGridTile TileData GetTile(NextTile); // 检查是否可通行 if (!TileData.IsWalkable()) { continue; } // 计算到达下一个格子的总代价 int32 NewCost CurrentCost TileData.MovementCost; // 如果代价在移动力范围内且是更优路径或未访问过 if (NewCost MovementPoints) { int32* ExistingCost CostSoFar.Find(NextTile); if (!ExistingCost || NewCost *ExistingCost) { CostSoFar.Add(NextTile, NewCost); TileQueue.Enqueue(NextTile); ReachableTiles.Add(NextTile); } } } } // 移除起点可选 ReachableTiles.Remove(StartTile); return ReachableTiles; }在可视化器中可以添加一个函数来高亮显示这些可到达的格子例如用蓝色半透明方块覆盖为玩家提供清晰的移动范围指示。7. 性能考量与优化方向当网格变大如50x50以上或需要每帧进行大量查询时性能变得重要。数据结构目前使用TArrayFGridTile线性存储访问是 O(1)。对于超大网格可以考虑空间分区如四叉树来加速区域查询但均匀网格的数组访问通常已足够高效。调试绘制开销DrawDebugBox和DrawDebugLine在开发期很好用但在发布版本中应移除或通过条件编译禁用。可以考虑使用实例化静态网格体ISM或动态网格来高效渲染固定网格。异步计算像CalculateMovementRange这样的函数如果计算量很大可以考虑放到异步任务如AsyncTask中执行避免阻塞游戏线程。数据与表现分离UBattleGrid只负责数据逻辑。ABattleGridVisualizer负责表现。这种分离使得我们可以在不改变核心逻辑的情况下更换更华丽的网格表现效果如用模型、粒子来表现格子。序列化可以将网格的初始状态地形、固定障碍保存为数据资产UDataAsset在游戏加载时读取方便关卡设计。8. 常见问题与排查方法在实现和使用网格系统时你可能会遇到以下问题问题现象可能原因排查方式解决方案编译失败提示找不到BattleGrid类1. 头文件未正确包含。2. 模块依赖未添加。1. 检查#include “BattleGrid.h”路径是否正确。2. 在项目的.Build.cs文件中确保PublicDependencyModuleNames包含了所需模块如CoreUObject,Engine。1. 使用相对于项目源文件目录的正确路径。2. 在YourProject.Build.cs中添加PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine});。网格在游戏中不显示1.BattleGridVisualizer的BattleGridData属性未赋值。2.bAlwaysShowGrid为 false。3. 调试绘制在发布模式或特定视口被禁用。1. 在编辑器细节面板检查属性是否设置。2. 检查bAlwaysShowGrid是否勾选。3. 在游戏运行时打开控制台输入showdebug 查看调试信息是否开启。1. 确保将BP_BattleGridData资产拖拽赋值。2. 勾选bAlwaysShowGrid。3. 确保在Project Settings - Engine - Console中Debug命令可用。也可考虑用GEngine-AddOnScreenDebugMessage打印日志辅助调试。坐标转换错误索引越界1. 世界坐标原点 (Origin) 设置错误。2.TileSize为0或负数。3. 世界坐标超出了网格定义的区域。1. 打印Origin,WorldLocation, 计算出的DeltaX/Y和X/YIndex。2. 检查GetTileIndexFromWorldLocation函数中的除法和取整逻辑。1. 确保Origin是网格的左下角点。2. 确保TileSize 0。3. 在转换前增加边界检查或返回一个无效索引标志。移动范围计算异常漏算或多算格子1.MovementCost设置错误如负数。2. BFS 算法中方向数组或边界检查有误。3.IsWalkable()判断逻辑与预期不符。1. 单步调试 BFS 算法观察队列和CostSoFar的变化。2. 打印每个被检查的NextTile的索引和通行状态。1. 确保MovementCost为正数。2. 检查方向数组是否包含了所有期望的相邻方向四连通或八连通。3. 复核EGridTileState枚举和IsWalkable()函数的对应关系。设置格子状态后可视化没有立即更新调试绘制 (DrawDebugBox) 每帧基于当前数据重新绘制。如果数据已更新但绘制未反映可能是绘制逻辑未读取最新状态。确认在DrawDebugGrid()函数中是通过BattleGridData-GetAllTiles()获取的实时数据。确保SetTileState修改的是Tiles数组中的元素而GetAllTiles()返回的是该数组的引用或副本。本例中返回的是const引用绘制时能读到最新值。如果仍有问题可在SetTileState中触发一个重绘事件。9. 最佳实践与后续扩展建议数据驱动设计将网格的配置大小、格子类型预设放在数据资产或表格如DataTable中方便策划调整无需重新编译代码。与导航系统结合对于复杂的非网格化移动需求可以将网格作为高层策略规划而将 UE 内置的NavMesh用于具体的路径寻路。例如用网格决定“去哪个区域”用NavMesh计算“具体怎么走过去”。分层网格对于多层建筑或高低差地形可以引入高度Z轴维度或者为每个楼层维护一个独立的网格。网络同步在多人游戏中网格状态如哪个格子被占据需要在客户端和服务器之间同步。可以考虑将UBattleGrid放在服务器上通过 RPC 或属性复制将关键状态同步给客户端。高级可视化替换简单的调试绘制使用静态网格体、材质实例动态改变颜色、或 Niagara 粒子系统来表现格子状态可通行、高亮、危险区域等提升游戏表现力。编辑器扩展可以开发一个自定义的编辑器模式EdMode让关卡设计师能更方便地在视口中绘制、编辑网格并设置格子属性。至此一个基础的、可运行的 UE5 C 战斗网格系统就搭建完成了。它提供了核心的数据管理、坐标转换、状态查询和范围计算能力并附带了可视化的调试工具。你可以以此为基础将其集成到你的角色移动系统、技能目标选择系统和战斗结算系统中逐步构建出完整的 CRPG 战斗体验。记住先让基础功能跑通再根据实际游戏需求进行迭代和优化。
返回列表