简介:这是一份基于Unreal Engine 4.22开发的3D塔防游戏完整工程资源,面向UE4初学者与C++游戏开发学习者,聚焦塔防类RTS游戏的核心逻辑实现与3D场景交互实践。资源包含可直接编译运行的UE4项目工程,涵盖炮塔部署、波次敌人生成、生命值管理、地图岩石障碍物交互等关键模块,代码以C++为主,辅以蓝图逻辑,适合边学边练、理解UE4游戏框架与塔防AI设计思路。压缩包为ZIP格式,大小1.01GB,含完整项目文件(如Config、Source、Content目录)、已集成的Epic官方敌人角色网格体及授权音频资源,无额外说明性文档或视频。目前已有647人学习下载,读者可获得可调试的实战项目源码、典型崩溃问题的临时解决方案(如地图重载操作)、清晰的波次难度递进设计及11关卡完整通关路径,是深入理解UE4塔防游戏架构与性能调优的优质入门范例。
1. 士兵-塔-防御-UE4:这不是一个“改个材质就能跑”的塔防Demo,而是一套可调试、可扩展、带完整AI路径寻路与塔升级逻辑的UE4 C++工程
你可能已经下载过十几个标着“UE4塔防”的GitHub项目——点开一看,全是蓝图拖拽的简易demo:三座塔、五种怪、路径硬编码在静态网格上,敌人走到终点不扣血,塔升级按钮点了没反应。这种项目连编译都常报错,更别说改逻辑。而“士兵-塔-防御-UE4”这个资源,是少数真正用C++重写核心模块的实战组合:敌人AI使用A*+局部避障双层导航,塔类继承自ATowerBase基类并实现IUpgradeable接口,伤害计算走FGameplayEffectSpec而非简单减血,连炮塔旋转延迟都做了时间插值平滑。它不是教学模板,而是某高校游戏实验室为验证UE4多线程AI调度能力所构建的最小可行系统(MVP),源码包里甚至保留了早期用TaskGraph重构路径计算的commit注释。适合想脱离蓝图舒适区、动手改战斗逻辑、理解UE4 Gameplay Ability System与Navigation System如何协同的中级以上开发者——如果你正卡在“怎么让塔自动瞄准移动目标”或“为什么敌人总卡在拐角”,这份工程就是你的调试沙盒。
2. 工程结构解析:从Build.cs到GameModeBase,看清C++模块如何分层协作
2.1 源码目录树与关键模块职责划分
整个工程采用标准UE4 C++项目结构,但关键模块做了显式分层。Source/SoldierTowerDefense/下并非扁平堆砌,而是按职责划分为:
| 目录路径 | 核心类 | 主要职责 | 是否含热重载敏感代码 |
|---|---|---|---|
Actors/ | ATowerBase,AEnemyBase,AProjectileBase | 所有可放置Actor的基类,定义通用行为接口(如CanFire(),GetTarget()) | 是(含Tick中调用UFUNCTION(BlueprintCallable)) |
AI/ | AEnemyAIController,UEnemyBTTask_MoveToPathPoint | 敌人AI控制器与行为树任务,路径点由UNavigationSystemV1::SimpleMoveToLocation触发,但实际移动由FVector::VInterpTo驱动 | 否(纯C++逻辑,无蓝图绑定) |
Gameplay/ | UGameplayAbility_TowerAttack,FTowerUpgradeData | 塔攻击能力实现与升级数据结构,FTowerUpgradeData含DamageIncreasePercent,FireRateMultiplier等可序列化字段 | 是(UPROPERTY(VisibleAnywhere)字段影响编辑器显示) |
Managers/ | UTowerManager,UEnemyWaveManager | 全局管理器单例,通过GetWorld()->GetSubsystem<UTowerManager>()访问,负责塔池复用与波次调度 | 否(Subsystem无Tick,仅提供服务接口) |
提示:
Managers/下的Subsystem类是本工程关键设计亮点——它规避了传统GameMode中臃肿的管理逻辑,把塔的建造/销毁/升级状态完全解耦。你修改塔升级效果时,只需动FTowerUpgradeData结构体,无需碰GameMode.cpp。
2.2 Build.cs配置与模块依赖关系
SoldierTowerDefense.Build.cs文件暴露了工程对UE4底层模块的真实依赖。除默认的Core,CoreUObject,Engine外,明确启用了:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "GameplayTasks", "NavigationSystem", "AIModule", "GameplayAbilities", "GameplayTags" });注意GameplayAbilities和GameplayTags的引入——这说明塔的攻击能力并非简单OnComponentBeginOverlap触发,而是通过UGameplayAbility系统派发FGameplayEffectSpecHandle应用伤害。若你本地UE4版本低于4.26,需手动在Edit > Editor Preferences > Loading & Saving中启用Gameplay Ability System插件,否则编译会因#include "GameplayAbilities/Public/GameplayAbility.h"失败。
2.3 GameMode与GameState的关键重写点
ASoldierTowerDefenseGameMode并未重写BeginPlay()做初始化,而是将核心逻辑下沉至ASoldierTowerDefenseGameState:
// SoldierTowerDefenseGameState.h UCLASS() class SOLDIERTOWERDEFENSE_API ASoldierTowerDefenseGameState : public AGameStateBase { GENERATED_BODY() public: UPROPERTY(Replicated) int32 CurrentWaveIndex; UPROPERTY(Replicated) float TimeUntilNextWave; // 关键:Wave数据由DataTable驱动,非硬编码 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Wave") UDataTable* WaveDataTable; // 指向/WaveData/WaveTable.DataTable virtual void OnRep_CurrentWaveIndex() override; };WaveDataTable指向一个.uasset数据表,其结构为FWaveDataRow(含WaveNumber,EnemyClass,EnemyCount,SpawnInterval)。这意味着你调整第5波敌人数量,只需双击编辑器中该行数据,无需重新编译。这是本工程区别于“伪C++项目”的实证——它真正利用了UE4的数据驱动设计范式。
3. 核心战斗逻辑实现:从敌人路径生成到塔升级状态机
3.1 敌人路径生成:A*算法与NavMesh的协同落地
敌人移动并非简单MoveToLocation,而是先由UEnemyWaveManager::GeneratePathPoints()调用A*求解器,再将结果缓存为TArray<FVector>供AI使用:
// EnemyWaveManager.cpp void UEnemyWaveManager::GeneratePathPoints() { // Step 1: 获取起点与终点(从Level Blueprint中获取Spline Component) const USplineComponent* PathSpline = GetWorld()->GetFirstPlayerController()->GetPawn()->FindComponentByClass<USplineComponent>(); if (!PathSpline) return; // Step 2: 采样Spline生成离散路径点(间隔100单位) PathPoints.Empty(); for (float Dist = 0.f; Dist < PathSpline->GetSplineLength(); Dist += 100.f) { FVector Point = PathSpline->GetLocationAtDistanceAlongSpline(Dist, ESplineCoordinateSpace::World); // Step 3: 投影到NavMesh确保可行走 FNavLocation NavLoc; if (UNavigationSystemV1::GetCurrent(GetWorld())->GetRandomPointInNavigableRadius(Point, 50.f, NavLoc)) { PathPoints.Add(NavLoc.Location); } } }关键点在于GetRandomPointInNavigableRadius——它确保每个路径点都在NavMesh有效区域内。若你发现敌人在拐角处卡死,首要检查NavMesh烘焙是否覆盖全部路径区域(Window > Navigation > Rebuild)。本工程NavMesh设置中Agent Radius为30,Agent Height为120,若你替换了更高模型,需同步调整。
3.2 塔攻击逻辑:Gameplay Ability系统实战
塔的攻击能力封装在UGameplayAbility_TowerAttack中,其ActivateAbility()触发流程如下:
// TowerAttack.cpp void UGameplayAbility_TowerAttack::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData); // 1. 获取目标(从黑板中读取,黑板Key为"TargetActor") AActor* Target = GetBlackboardValueAsObject("TargetActor"); // 2. 构建GameplayEffectSpec(伤害值来自塔实例的CurrentDamage属性) FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec( DamageEffectClass, // .uasset路径:/Game/Effects/GES_TowerDamage.GES_TowerDamage ActorInfo->AbilitySystemComponent.Get()->GetLevel() ); // 3. 动态设置伤害量(避免硬编码) SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag("Data.Damage"), Cast<ATowerBase>(ActorInfo->AvatarActor.Get())->GetCurrentDamage()); // 4. 应用效果 ActorInfo->AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get(), 0); }注意:
DamageEffectClass必须是继承自UGameplayEffect的类,且其Modifiers中需添加Damage标签的SetByCaller数值。若你修改了塔的伤害公式,只需改GetCurrentDamage()返回值,无需动GameplayEffect资产。
3.3 塔升级状态机:基于UPROPERTY的序列化状态流转
塔的升级逻辑由ATowerBase::UpgradeTower()驱动,其状态存储在FTowerUpgradeData结构体中:
// TowerBase.h USTRUCT(BlueprintType) struct FTowerUpgradeData { GENERATED_BODY() UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Upgrade") int32 MaxUpgradeLevel; UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Upgrade") TArray<FUpgradeStat> UpgradeStats; // 如{Damage, FireRate, Range}每级增幅 }; // TowerBase.cpp void ATowerBase::UpgradeTower() { if (CurrentUpgradeLevel >= UpgradeData.MaxUpgradeLevel) return; CurrentUpgradeLevel++; // 动态更新属性(此处为示例,实际调用UpdateStatsFromUpgradeLevel) UpdateStatsFromUpgradeLevel(CurrentUpgradeLevel); // 关键:广播升级事件,UI通过Delegate响应 OnTowerUpgraded.Broadcast(this, CurrentUpgradeLevel); }UpgradeStats数组在编辑器中可直接展开编辑,每项FUpgradeStat含StatName(FName),BaseValue(float),IncrementPerLevel(float)。这种设计让你能为不同塔类型配置差异化升级曲线——例如狙击塔Range增幅大但FireRate增幅小,而机枪塔反之。
4. 避坑指南:五个真实踩过的坑与血泪修复方案
4.1 现象:敌人AI在路径拐角处原地旋转,不向前移动
原因:AEnemyAIController中MoveToLocation的目标点未做NavMesh投影,导致目标点落在不可行走区域,AI陷入FAIMoveRequest::Status == EPathFollowingStatus::Invalid状态。
解决:在AEnemyAIController::MoveToNextPathPoint()中,强制对目标点执行NavMesh查询:
FNavLocation NavLoc; if (UNavigationSystemV1::GetCurrent(GetWorld())->ProjectPointOnNavMesh( GetPawn()->GetActorLocation(), NavLoc, 50.f)) { MoveToLocation(NavLoc.Location, ...); // 此处才调用MoveToLocation } else { // 备用方案:沿当前朝向前进一小段 FVector Forward = GetPawn()->GetActorForwardVector(); MoveToLocation(GetPawn()->GetActorLocation() + Forward * 100.f, ...); }4.2 现象:塔升级后UI显示等级正确,但实际伤害/射速未变化
原因:ATowerBase::UpdateStatsFromUpgradeLevel()中未调用GetWorld()->GetTimerManager().ClearTimer(AttackTimerHandle)重置攻击计时器,导致旧的SetTimer仍在运行。
解决:在UpgradeTower()末尾强制清除并重启计时器:
GetWorld()->GetTimerManager().ClearTimer(AttackTimerHandle); GetWorld()->GetTimerManager().SetTimer(AttackTimerHandle, this, &ATowerBase::FireProjectile, GetCurrentFireRate(), true);4.3 现象:多人游戏下,塔升级状态不同步,客户端看到的等级与服务器不一致
原因:CurrentUpgradeLevel未标记为UPROPERTY(Replicated),且未实现OnRep_CurrentUpgradeLevel()回调。
解决:在头文件中修改声明,并添加同步回调:
// TowerBase.h UPROPERTY(Replicated, BlueprintReadOnly, Category = "Upgrade") int32 CurrentUpgradeLevel; // TowerBase.cpp void ATowerBase::OnRep_CurrentUpgradeLevel() { // 服务端调用UpdateStatsFromUpgradeLevel()已足够,客户端需额外刷新UI if (IsValid(TowerUIWidget)) { TowerUIWidget->UpdateLevelDisplay(CurrentUpgradeLevel); } }4.4 现象:编译通过,但运行时崩溃于UGameplayAbility_TowerAttack::ActivateAbility(),调用栈指向GetBlackboardValueAsObject
原因:黑板(Blackboard)未在AI Controller构造函数中初始化,或TargetActorKey未在黑板数据资产中定义。
解决:在AEnemyAIController构造函数中显式设置黑板:
// EnemyAIController.cpp AEnemyAIController::AEnemyAIController() { static ConstructorHelpers::FObjectFinder<UBlackboardData> BBObject(TEXT("/Game/AI/BB_Enemy.BB_Enemy")); if (BBObject.Succeeded()) { BBAsset = BBObject.Object; } }并在编辑器中打开BB_Enemy.Blackboard,确认存在TargetActor变量(类型为Object)。
4.5 现象:切换地图后,UTowerManager子系统中的塔列表为空,新建塔无法注册
原因:UTowerManager被声明为UWorldSubsystem,但未在DefaultEngine.ini中启用自动加载。
解决:在项目配置文件Config/DefaultEngine.ini末尾添加:
[/Script/Engine.World] WorldSubsystemClasses=SoldierTowerDefense.TowerManager并确保TowerManager.h中UCLASS()宏含AutoLoadSingleton标识:
UCLASS(AutoLoadSingleton, Blueprintable) class UTowerManager : public UWorldSubsystem { GENERATED_BODY() };5. 进阶技巧:用Data Asset驱动塔类型配置,彻底告别硬编码
5.1 创建塔类型数据资产(Data Asset)
本工程已预留/Game/DataAssets/TowerTypes/目录,但未填充具体资产。你需要创建UTowerTypeData类来承载塔的元数据:
// TowerTypeData.h UCLASS(ClassGroup = "Tower", BlueprintType, meta = (BlueprintSpawnableComponent)) class SOLDIERTOWERDEFENSE_API UTowerTypeData : public UDataAsset { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Tower") TSubclassOf<ATowerBase> TowerClass; // 对应的C++塔类 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Tower") FText TowerName; // 编辑器中显示名称 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Tower") float BaseCost; // 基础建造费用 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Tower") TArray<FString> Tags; // 如{"Sniper", "Splash"},用于筛选 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Tower") UTexture2D* IconTexture; // UI图标 };编译后,在编辑器中右键/Game/DataAssets/TowerTypes/→Create > SoldierTowerDefense > Tower Type Data,命名为TD_SniperTower。在细节面板中设置TowerClass为BP_SniperTower(若存在蓝图子类)或ASniperTower(C++类),填写BaseCost=200,Tags={"Sniper"}。
5.2 在建造系统中动态加载Data Asset
ATowerManager的SpawnTower()方法需改造以支持Data Asset驱动:
// TowerManager.cpp ATowerBase* UTowerManager::SpawnTower(const UTowerTypeData* TowerType, const FVector& Location) { if (!TowerType || !TowerType->TowerClass) return nullptr; FActorSpawnParameters SpawnParams; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; ATowerBase* NewTower = GetWorld()->SpawnActor<ATowerBase>( TowerType->TowerClass, Location, FRotator::ZeroRotator, SpawnParams); if (NewTower) { // 将Data Asset中的基础参数注入塔实例 NewTower->SetBaseCost(TowerType->BaseCost); NewTower->SetTowerName(TowerType->TowerName); // 关键:注册到管理器,供后续升级/销毁使用 RegisteredTowers.Add(NewTower); } return NewTower; }此时,建造逻辑完全解耦:UI点击“狙击塔”按钮时,不再硬编码SpawnActor<ASniperTower>,而是查找TD_SniperTower资产并传入SpawnTower()。新增塔类型只需创建新Data Asset,无需修改一行C++代码。
5.3 用DataTable实现波次配置的可视化编辑
工程中/Game/WaveData/WaveTable.DataTable已存在,但结构需扩展以支持Data Asset引用。在FWaveDataRow中添加:
// WaveDataRow.h USTRUCT(BlueprintType) struct FWaveDataRow { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Wave") int32 WaveNumber; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Wave") TSoftClassPtr<AEnemyBase> EnemyClass; // 改为SoftClassPtr,支持热重载 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Wave") int32 EnemyCount; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Wave") float SpawnInterval; // 新增:支持多种敌人混合波次 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Wave") TArray<FEnemySpawnEntry> EnemySpawnEntries; // 自定义结构体 }; USTRUCT(BlueprintType) struct FEnemySpawnEntry { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn") TSoftClassPtr<AEnemyBase> EnemyClass; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn") int32 Count; };编辑WaveTable.DataTable时,EnemySpawnEntries可展开添加多行,每行指定不同敌人类型及数量。UEnemyWaveManager::SpawnWave()需遍历此数组而非单一EnemyClass,实现真正的混合波次。
从那以后我每次新增塔类型,都强制走一遍Data Asset创建→编辑器赋值→UI按钮绑定→运行验证四步流程,哪怕只是改个图标。因为UE4的Data Asset机制是唯一能让我在不触碰C++编译的前提下,把策划需求当天落地的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取