原文:
My name is Dmitry and I'm a programmer at Snowforged Entertainment. Our team is currently working on Starfall Tactics – an upcoming online RTS game based on the Unreal Engine 4. I’ve just finished refactoring a movement component for spaceships. This component had to be rewritten three times, going back all the way to the start of development on the game to working on the current alpha version.
During this period of time, a lot of mistakes were made and painful lessons learned. In this article, I’d like to share my experience with you and talk about Navigation Volumes, Movement component, AIController and Pawn.
Objective: implement spaceship movement on a given plane.
Things to consider:
First, let’s consider the elements involved in finding an optimal path via Unreal Engine 4. UShipMovementComponent is a movement component inherited from UPawnMovementComponent, due to the end unit (in this case, our spaceship) being derived from APawn.
UPawnMovementComponent is originated from UNavMovementComponent, which contains the FNavProperties field. These are navigational parameters that describe the shape of a given APawn that is also used by the AIController when searching for pathways.
Suppose we have a level that contains a spaceship, some static objects and Navigation Volume. We decide to send this spaceship from one point of the map to another. This is what happens inside UE4:
1) APawn finds within itself the ShipAIController (in our case, it's just the class that was derived from AIController, having just a single method) and calls the method for seeking pathways.
2) In this method we first prepare a request to the navigation system. Then, after sending the request, we receive movement control points.
3) These points are then returned as an array to APawn, in a format that is convenient for us (FVector). Finally, the movement begins.
In a nutshell: APawn contains a ShipAIController, which at the time of the calling of PreparePathfinding() refers to APawn and receives theUShipMovementComponent, containing FNavProperties that address the navigation system in order to find the optimal path.
So, we’ve just received a list of movement control points. The first point is always the spaceship’s current position, the latter - our destination. In this case, the place on the game map where we clicked with the cursor.
I should probably tell you a little bit about how we plan to interface with the network. For the sake of simplicity, let’s break up the process into several steps and describe each one:
1) We call the function responsible for starting the movement - AShip::CommandMoveTo():
Pay close attention. On the client’s side, all Pawns are missing an AIController, which exists only on the server. So when the client calls the function to send the ship to a new location, all calculations should be done server-side. In other words, the server is responsible for seeking the optimal path for each spaceship because it is the AIController that works with the navigation system.
2) After we’ve found a list of control points inside the CommandMoveTo() method, we call the next one to start moving the selected spaceship. This method should be called on all clients.
In this method, a client that does not have any control points adds the fist received coordinate to the list of control points and “starts the engine”, moving the ship into motion. Using timers, we activate the process of sending the remaining intermediate and end points for this particular journey on the server:
On the client’s side, while the ship starts to accelerate and move to the first control point, it gradually receives the remaining control points and adds them to an array. This takes some load off the network and allows us to stretch the time it takes to send data, thus distributing the load.
Alright, enough with the supplementary info. Let’s get back to business ;) Our current task – make the ship move towards the nearest control point. Keep in mind that our ship has rotational speed, as well as a speed of acceleration.
When you send the spaceship to a new destination, it could be flying at full speed, staying still, accelerating or be in the process of turning in that particular moment. Therefore, we have to act depending on current speed characteristics and destination.
We have identified three types of spaceship behavior:
Before the ship starts moving to a control point, we need to decide on the speed parameters to be used. To achieve this, we’ve implemented a function for simulating a flight. I’d rather skip explaining the code in this article, but if you need more information on this, just drop me a message.
The principles are quite simple - using the current DeltaTime, we keep moving the vector of our position and rotate the forward vector, simulating the vessel’s rotation. It’s quite a simple operation that utilizes vectors and FRotator.
I should probably mention that in each iteration of the ship’s rotation, we should track and accumulate the angle of rotation. If it’s more than 180 degrees, it means that the spaceship has started moving in circles around the end point, so we should probably try the next set of speed parameters.
At first, the spaceship tries to fly at full speed and then at reduced speed (we are currently using average speed). If none of these solutions work – the ship simply needs to rotate in order to align with its destination and fly towards it.
Please keep in mind that all of the logic in the assessment of the situation and the movement processes should be implemented in AShip. This is due to the AIController missing on the client’s side, as well as UShipMovementComponent playing a different role (which I’ll talk about soon). So to make sure that our spaceships can move on their own, without constantly synchronizing coordinates with the server, we need to realize the movement logic withinAShip.
Now, let’s talk about the most important part of the whole process - our movement component UShipMovementComponent. You should keep in mind that components of this type are just like engines. Their function is moving the object forward and rotating it when necessary, without worrying about what kind of logic should the object rely on for movement or what state it is in. Once again, they are only responsible for the actual movement of the said subject.
The gist of using UMovementComponent and its derived classes is as follows: we use a given Tick() to make all mathematical calculations related to our component’s parameters (speed, maximum speed, rotational speed). We then set the UMovementComponent::Velocity parameter to a value that is relevant to the spaceship’s transposition at this time tick. Then, we call UMovementComponent::MoveUpdatedComponent(), where the actual transposition and rotation of the spaceship occurs.
A few words about the states that appear in this article. They are needed to combine the various processes related to movement. For example, when reducing speed (to have enough space for maneuvering, we need to move at average speed) and rotating towards a new destination.
In the movement component, states are used only for evaluation purposes: should we continue accelerating or should we decrease momentum, etc.
All of the logic related to the transition from one state of motion to another is done via AShip. For example, the spaceship is flying at full speed and the final destination has changed, so we need to lower the vessel’s speed to its average value.
And a quick word about AcceptedRotator. It's the ship’s rotation at the current time tick. In the AShip time tick we call the following method of theUShipMovementComponent:
RotateTo = (GoalLocation - ShipLocation).Rotation() - ie this is a rotator that denotes what rotation value the vessel should be at in order for it to face the end point.
In this method, we evaluate whether the spaceship is already looking at the destination. In that case, this result is returned and the vessel will not rotate. In its assessment of the situation, AShip will reset the state EShipMovementState::Turning - and UShipMovementComponent will no longer attempt to rotate. Otherwise, we use the rotation and interpret based on DeltaTime and the spaceship’s rotational speed. Then apply this rotation to the current time tick, when calling UMovementComponent::MoveUpdatedComponent().
In my humble opinion, this particular version of UShipMovementComponent takes into account all of the problems our team faced during the prototyping stage of Starfall Tactics. As an added bonus, the solution turned out to be quite scalable and there is an opportunity to improve it further.
Take, for example, the moment when the spaceship is turning: if we simply rotate the ship, it looks dull, as if the vessel is attached to a piece of string. However, allowing the spaceship to dip slightly in the direction of the turn results in an attractive and fluid action.
Also, the synchronization of intermediate spaceship positions is realized on a workable level. As soon as the object reaches its destination, the data is synchronized with the server. So far, the difference in the vessel's final position on the server and the client is fairly small. However, if miscalculations start occurring more frequently, I have a lot of ideas on how to improve synchronization without spaceships performing sudden “jumps”. I guess we’ll talk about them another time.
During this period of time, a lot of mistakes were made and painful lessons learned. In this article, I’d like to share my experience with you and talk about Navigation Volumes, Movement component, AIController and Pawn.
Objective: implement spaceship movement on a given plane.
Things to consider:
- Spaceships in Starfall Tactics have a maximum speed, rotational speed and a rate of acceleration. These parameters directly influence the ship’s movement.
- You have to rely on Navigation Volume to automatically search for obstacles and decide on the safest path.
- There shouldn’t be continuous synchronization of position coordinates across the network.
- Spaceships can start moving from different speed states.
- Everything must be done natively with regards to the architecture of Unreal Engine 4.
Task 1 - Searching for the optimal path
First, let’s consider the elements involved in finding an optimal path via Unreal Engine 4. UShipMovementComponent is a movement component inherited from UPawnMovementComponent, due to the end unit (in this case, our spaceship) being derived from APawn.
UPawnMovementComponent is originated from UNavMovementComponent, which contains the FNavProperties field. These are navigational parameters that describe the shape of a given APawn that is also used by the AIController when searching for pathways.
Suppose we have a level that contains a spaceship, some static objects and Navigation Volume. We decide to send this spaceship from one point of the map to another. This is what happens inside UE4:
1) APawn finds within itself the ShipAIController (in our case, it's just the class that was derived from AIController, having just a single method) and calls the method for seeking pathways.
2) In this method we first prepare a request to the navigation system. Then, after sending the request, we receive movement control points.
TArray<FVector> AShipAIController::SearchPath(const FVector& location) { FPathFindingQuery Query; const bool bValidQuery = PreparePathfinding(Query, location, NULL); UNavigationSystem* NavSys = UNavigationSystem::GetCurrent(GetWorld()); FPathFindingResult PathResult; TArray<FVector> Result; if(NavSys) { PathResult = NavSys->FindPathSync(Query); if(PathResult.Result != ENavigationQueryResult::Error) { if(PathResult.IsSuccessful() && PathResult.Path.IsValid()) { for(FNavPathPoint point : PathResult.Path->GetPathPoints()) { Result.Add(point.Location); } } } else { DumpToLog("Pathfinding failed.", true, true, FColor::Red); } } else { DumpToLog("Can't find navigation system.", true, true, FColor::Red); } return Result; }
3) These points are then returned as an array to APawn, in a format that is convenient for us (FVector). Finally, the movement begins.
In a nutshell: APawn contains a ShipAIController, which at the time of the calling of PreparePathfinding() refers to APawn and receives theUShipMovementComponent, containing FNavProperties that address the navigation system in order to find the optimal path.
Task 2 - Implementing movement to the end point
So, we’ve just received a list of movement control points. The first point is always the spaceship’s current position, the latter - our destination. In this case, the place on the game map where we clicked with the cursor.
I should probably tell you a little bit about how we plan to interface with the network. For the sake of simplicity, let’s break up the process into several steps and describe each one:
1) We call the function responsible for starting the movement - AShip::CommandMoveTo():
UCLASS() class STARFALL_API AShip : public APawn, public ITeamInterface { ... UFUNCTION(BlueprintCallable, Server, Reliable, WithValidation, Category = "Ship") void CommandMoveTo(const FVector& location); void CommandMoveTo_Implementation(const FVector& location); bool CommandMoveTo_Validate(const FVector& location); ... }
Pay close attention. On the client’s side, all Pawns are missing an AIController, which exists only on the server. So when the client calls the function to send the ship to a new location, all calculations should be done server-side. In other words, the server is responsible for seeking the optimal path for each spaceship because it is the AIController that works with the navigation system.
2) After we’ve found a list of control points inside the CommandMoveTo() method, we call the next one to start moving the selected spaceship. This method should be called on all clients.
UCLASS() class STARFALL_API AShip : public APawn, public ITeamInterface { ... UFUNCTION(BlueprintCallable, Reliable, NetMulticast, Category = "Ship") void StartNavMoveFrom(const FVector& location); virtual void StartNavMoveFrom_Implementation(const FVector& location); ... }
In this method, a client that does not have any control points adds the fist received coordinate to the list of control points and “starts the engine”, moving the ship into motion. Using timers, we activate the process of sending the remaining intermediate and end points for this particular journey on the server:
void AShip::CommandMoveTo(const FVector& location) { ... GetWorldTimerManager().SetTimer(timerHandler, FTimerDelegate::CreateUObject(this, &AShip::SendNextPathPoint), 0.1f, true); ... }
UCLASS() class STARFALL_API AShip : public APawn, public ITeamInterface { ... FTimerHandle timerHandler; UFUNCTION(BlueprintCallable, Reliable, NetMulticast, Category = "Ship") void SendPathPoint(const FVector& location); virtual void SendPathPoint_Implementation(const FVector& location); ... }
On the client’s side, while the ship starts to accelerate and move to the first control point, it gradually receives the remaining control points and adds them to an array. This takes some load off the network and allows us to stretch the time it takes to send data, thus distributing the load.
Alright, enough with the supplementary info. Let’s get back to business ;) Our current task – make the ship move towards the nearest control point. Keep in mind that our ship has rotational speed, as well as a speed of acceleration.
When you send the spaceship to a new destination, it could be flying at full speed, staying still, accelerating or be in the process of turning in that particular moment. Therefore, we have to act depending on current speed characteristics and destination.
We have identified three types of spaceship behavior:
- The vessel can fly to the end point at full speed and fall back on rotational speed to arrive at its destination.
- The spaceship’s current speed might be too fast, so it will try to align with its destination using average speed. When the ship directly faces its destination, it will accelerate and try to reach the target at maximum speed.
- If the other pathways takes more time than simply rotating on the spot and flying to the target in a straight line, the vessel will proceed to do so.
Before the ship starts moving to a control point, we need to decide on the speed parameters to be used. To achieve this, we’ve implemented a function for simulating a flight. I’d rather skip explaining the code in this article, but if you need more information on this, just drop me a message.
The principles are quite simple - using the current DeltaTime, we keep moving the vector of our position and rotate the forward vector, simulating the vessel’s rotation. It’s quite a simple operation that utilizes vectors and FRotator.
I should probably mention that in each iteration of the ship’s rotation, we should track and accumulate the angle of rotation. If it’s more than 180 degrees, it means that the spaceship has started moving in circles around the end point, so we should probably try the next set of speed parameters.
At first, the spaceship tries to fly at full speed and then at reduced speed (we are currently using average speed). If none of these solutions work – the ship simply needs to rotate in order to align with its destination and fly towards it.
Please keep in mind that all of the logic in the assessment of the situation and the movement processes should be implemented in AShip. This is due to the AIController missing on the client’s side, as well as UShipMovementComponent playing a different role (which I’ll talk about soon). So to make sure that our spaceships can move on their own, without constantly synchronizing coordinates with the server, we need to realize the movement logic withinAShip.
Now, let’s talk about the most important part of the whole process - our movement component UShipMovementComponent. You should keep in mind that components of this type are just like engines. Their function is moving the object forward and rotating it when necessary, without worrying about what kind of logic should the object rely on for movement or what state it is in. Once again, they are only responsible for the actual movement of the said subject.
The gist of using UMovementComponent and its derived classes is as follows: we use a given Tick() to make all mathematical calculations related to our component’s parameters (speed, maximum speed, rotational speed). We then set the UMovementComponent::Velocity parameter to a value that is relevant to the spaceship’s transposition at this time tick. Then, we call UMovementComponent::MoveUpdatedComponent(), where the actual transposition and rotation of the spaceship occurs.
void UShipMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if(!PawnOwner || !UpdatedComponent || ShouldSkipUpdate(DeltaTime)) { return; } if (CheckState(EShipMovementState::Accelerating)) { if (CurrentSpeed < CurrentMaxSpeed) { CurrentSpeed += Acceleration; AccelerationPath += CurrentSpeed*DeltaTime; } else { CurrentSpeed = CurrentMaxSpeed; RemoveState(EShipMovementState::Accelerating); } } else if (CheckState(EShipMovementState::Braking)) { if (CurrentSpeed > 0.0f) { CurrentSpeed -= Acceleration; DeaccelerationPath += CurrentSpeed*DeltaTime; } else { CurrentSpeed = 0.0f; CurrentMaxSpeed = MaxSpeed; RemoveState(EShipMovementState::Braking); RemoveState(EShipMovementState::Moving); } } else if (CheckState(EShipMovementState::SpeedDecreasing)) { if (CurrentSpeed > CurrentMaxSpeed) { CurrentSpeed -= Acceleration; DeaccelerationPath += CurrentSpeed*DeltaTime; } else { CurrentSpeed = CurrentMaxSpeed; RemoveState(EShipMovementState::SpeedDecreasing); } } if (CheckState(EShipMovementState::Moving) || CheckState(EShipMovementState::Turning)) { MoveForwardWithCurrentSpeed(DeltaTime); } } ... void UShipMovementComponent::MoveForwardWithCurrentSpeed(float DeltaTime) { Velocity = UpdatedComponent->GetForwardVector() * CurrentSpeed * DeltaTime; MoveUpdatedComponent(Velocity, AcceptedRotator, false); UpdateComponentVelocity(); }
A few words about the states that appear in this article. They are needed to combine the various processes related to movement. For example, when reducing speed (to have enough space for maneuvering, we need to move at average speed) and rotating towards a new destination.
In the movement component, states are used only for evaluation purposes: should we continue accelerating or should we decrease momentum, etc.
All of the logic related to the transition from one state of motion to another is done via AShip. For example, the spaceship is flying at full speed and the final destination has changed, so we need to lower the vessel’s speed to its average value.
And a quick word about AcceptedRotator. It's the ship’s rotation at the current time tick. In the AShip time tick we call the following method of theUShipMovementComponent:
bool UShipMovementComponent::AcceptTurnToRotator(const FRotator& RotateTo) { if(FMath::Abs(RotateTo.Yaw - UpdatedComponent->GetComponentRotation().Yaw) < 0.1f) { return true; } FRotator tmpRot = FMath::RInterpConstantTo(UpdatedComponent->GetComponentRotation(), RotateTo, GetWorld()->GetDeltaSeconds(), AngularSpeed); AcceptedRotator = tmpRot; return false; }
RotateTo = (GoalLocation - ShipLocation).Rotation() - ie this is a rotator that denotes what rotation value the vessel should be at in order for it to face the end point.
In this method, we evaluate whether the spaceship is already looking at the destination. In that case, this result is returned and the vessel will not rotate. In its assessment of the situation, AShip will reset the state EShipMovementState::Turning - and UShipMovementComponent will no longer attempt to rotate. Otherwise, we use the rotation and interpret based on DeltaTime and the spaceship’s rotational speed. Then apply this rotation to the current time tick, when calling UMovementComponent::MoveUpdatedComponent().
Future Prospects
In my humble opinion, this particular version of UShipMovementComponent takes into account all of the problems our team faced during the prototyping stage of Starfall Tactics. As an added bonus, the solution turned out to be quite scalable and there is an opportunity to improve it further.
Take, for example, the moment when the spaceship is turning: if we simply rotate the ship, it looks dull, as if the vessel is attached to a piece of string. However, allowing the spaceship to dip slightly in the direction of the turn results in an attractive and fluid action.
Also, the synchronization of intermediate spaceship positions is realized on a workable level. As soon as the object reaches its destination, the data is synchronized with the server. So far, the difference in the vessel's final position on the server and the client is fairly small. However, if miscalculations start occurring more frequently, I have a lot of ideas on how to improve synchronization without spaceships performing sudden “jumps”. I guess we’ll talk about them another time.
About the Author(s)
Senior programmer at Snowforged Entertainment.
License
GDOL (Gamedev.net Open License)
相关推荐
UE4 network Character Movement UE4 network Character MovementUE4 network Character MovementUE4 network Character MovementUE4 network Character Movement
虚幻引擎4(Unreal Engine 4,简称UE4)是一款强大的实时3D创作工具,广泛应用于游戏开发、影视制作、虚拟现实等多个领域。UE4的API是其核心功能之一,提供了丰富的编程接口,允许开发者通过C++或者蓝图系统进行深度...
在“Learning.C++.by.Creating.Games.with.UE4.Code.zip”这个压缩包中,我们可以推测它包含了一系列关于使用C++在虚幻4中开发游戏的教程代码。通过这些代码,学习者可以逐步理解如何利用C++在虚幻4环境下创建游戏。...
本资源"JSBSimForUe4_ue4_JSBSim_bankxry_UE4JSBSIM_源码.rar.rar"提供的正是JSBSim与UE4集成的源代码,对于开发者而言,这是一个宝贵的参考和学习材料。通过解压文件,我们可以深入研究如何将复杂的飞行模型与UE4的...
在UE4(Unreal Engine 4)开发过程中,有时会遇到WebBrowser组件无法播放H.264视频格式的问题。H.264是一种广泛使用的高效视频编码标准,但在某些UE4版本中,内置的Chromium Embedded Framework (CEF)可能不支持这种...
本主题聚焦于“UE4程序嵌入WINFORM”,这是一种将强大的虚幻引擎4(UE4)游戏引擎集成到Windows桌面应用(WinForm)中的方法。这种技术可以为桌面应用带来丰富的三维可视化和交互体验。 UE4是Epic Games开发的一款...
毕业设计基于C++的一款UE4射击游戏源码。一款UE4射击游戏Demo,包含UE4游戏框架及整套联网射击游戏功能。一款UE4射击游戏Demo,包含UE4游戏框架及整套联网射击游戏功能。一款UE4射击游戏Demo,包含UE4游戏框架及整套...
《UE4拾色器插件:MyColorPicker4.22》 UE4(Unreal Engine 4)是一款由Epic Games开发的高级游戏引擎,以其强大的3D渲染能力和丰富的功能集而广受开发者喜爱。在UE4的开发过程中,为了提高效率和用户体验,开发者...
《UE4植物资源包:构建逼真自然场景的利器》 在游戏开发和虚拟环境设计中,逼真的植物元素是不可或缺的部分。"UE4植物资源包"正为此目的而生,它提供了丰富的草、花、树以及其他自然植物模型,极大地便利了UE4用户...
在UE4(Unreal Engine 4)开发过程中,有时候我们需要获取电脑的唯一标识符或硬件信息,例如在实现用户认证、设备绑定等场景时。在UE4 4.25及之后的版本中,蓝图系统不再支持直接获取这些信息,这给开发者带来了一定...
基于C++开发UE4魔兽RPG风格的RTS游戏模板源码.zip基于C++开发UE4魔兽RPG风格的RTS游戏模板源码.zip基于C++开发UE4魔兽RPG风格的RTS游戏模板源码.zip基于C++开发UE4魔兽RPG风格的RTS游戏模板源码.zip基于C++开发UE4...
此外,“BIM-VR 1.0 For UE4.docx”文档很可能是详细的操作指南,它会指导用户如何正确使用工具,包括安装、设置、导入模型以及在UE4中进行进一步的编辑和优化。这份文档可能还会涵盖一些常见问题的解决方案,以帮助...
【UE4(虚幻4)制作跑酷小游戏】 UE4,全称为Unreal Engine 4,是由Epic Games开发的一款强大的实时3D创作工具,广泛应用于游戏开发、影视特效、建筑可视化等领域。它以其高效的渲染引擎、直观的蓝图系统和丰富的...
《UE4官方中文文档》是Unreal Engine 4(简称UE4)用户的重要参考资料,它提供了详尽的技术指导和教程,帮助开发者深入了解和熟练运用这款强大的游戏开发引擎。UE4是由Epic Games开发并维护的,它在游戏、影视、建筑...
在现代游戏开发和虚拟现实应用中,Unreal Engine 4(简称UE4)因其强大的图形渲染能力和丰富的工具集而备受青睐。UE4不仅用于创建高质量的3D游戏,还广泛应用于构建互动式Web应用程序和虚拟体验。本篇将深入探讨UE4...
虚幻4(UE4)是Epic Games开发的一款强大的实时3D创作工具,广泛应用于游戏开发、虚拟现实(VR)、电影特效等领域。本教程聚焦于UE4的一些实用小技巧,帮助用户快速上手并提升工作效率,使项目更具专业感。 1. **...
基于UE4开发的2D飞行射击小游戏源码+项目说明.zip基于UE4开发的2D飞行射击小游戏源码+项目说明.zip基于UE4开发的2D飞行射击小游戏源码+项目说明.zip基于UE4开发的2D飞行射击小游戏源码+项目说明.zip基于UE4开发的2D...