Closed PedrelliLuca closed 2 years ago
In the old ScalarField game, each thermodynamic component searches for its colder neighbors and updates both its and their temperatures, one at a time. As I'll show, this approach has a series of problems. Consider the following scenario:
The first interaction that occurs is A-B
, and the second is B-C
When B
interacts with C
, it doesn't have the temperature T(B)
, but a higher one whose value is between the original T(B)
and the original T(A)
. Since, in the real world, the two heat exchanges A-B
and B-C
would occur simultaneously with the temperature T(B)
, we're not correctly reproducing physics. This is a small error that, repeated thousands of times, can lead to very problematic situations where cold bodies become hotter than hot bodies, which could occur in the old ScalarField.
Another issue is that there's no guarantee on the engine's ticking order, and the ticking order matters a lot in this approach. In the image above I assumed the order A -> B -> C
, but what would happen with the ordering B -> A -> C
? First, the B-C
interaction would occur, leading to a temperature of B
lower than T(B)
. A
would then interact with a body B
colder than the A -> B -> C
scenario, making the game non-deterministic.
The problems above can be fixed by taking the following actions:
Tcurr
and the one for the next frame Tnext
Tcurr
s and produces a new value for this component's Tnext
.Tnext
, not the ones of the neighbor components.Of course, this only works as long as, before the beginning of the next frame, the Tcurr
value of every component has been substituted with the newly-computed Tnext
.
Tcurr = Tnext
?Let's say component A
is interacting with components X1
, ..., Xn
. A
knows that the Xs
are n
thanks to GetOverlappingComponents()
. If we make every Xi
increase a counter internal to A
during the Xi
's tick time, we can keep track of how many of them already interacted with A
: when the counter reaches n
, we can set A
's Tcurr
equal to Tnext
. However, there is an additional trick: n
is set to infinite until A's tick. so that if the X1
, ..., Xn
all tick before A
, it's not the Xn
that would trigger A
's assignment Tcurr = Tnext
(that would cause a bug since Tnext
is computed during A
's tick).
Here's a first draft of the UML:
@startuml
class UThermodynamicComponent {
+ void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override
+ double GetTemperature() const
+ void SetTemperature(double temperature)
+ void IncreaseInteractorsCount()
--
- double _currentTemperature
- double _nextTemperature
- double _heatCapacity
- static constexpr double _rodConstant
- uint32 _numberOfInteractors
- uint32 _interactorsCounter
}
UCapsuleComponent <|-- UThermodynamicComponent
@enduml
Below is a UML update
@startuml
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam sequenceParticipant underline
skinparam linetype ortho
class UThermodynamicComponent {
+ UThermodynamicComponent(const FObjectInitializer& objectInitializer)
+ void TickComponent(float deltaTime, ELevelTick tickType, FActorComponentTickFunction* thisTickFunction) override
+ void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override
+ double GetTemperature() const
+ FOnTemperatureChanged OnTemperatureChanged
--
# double _heatCapacity
# double _initialTemperature
--
- void _setTemperature(double temperature)
- void _setHeatCapacity(double heatCapacity)
- double _getTemperatureDelta(const TArray<TObjectPtr<UPrimitiveComponent>>& overlappingComponents, float deltaTime)
- void _increaseInteractorsCount()
- void _setCurrentTempAsNext()
- double _currentTemperature
- double _nextTemperature
- static constexpr double ROD_CONSTANT
- uint32 _numOfInteractors
- uint32 _counterOfInteractors
}
class FOnTemperatureChanged << (D, orchid) >>
class FColorizer {
+ static FLinearColor GenerateColorFromTemperature(double temperature)
--
- static FLinearColor _interpolateInRGB(double value, double min, double max, FLinearColor colorMin, FLinearColor colorMax)
}
class AThermodynamicActor {
+ AThermodynamicActor()
--
# void BeginPlay() override
--
- void _updateMaterialBasedOnTemperature(double temperature)
- TObjectPtr<UStaticMeshComponent> _staticMesh
- TObjectPtr<UThermodynamicComponent> _thermodynamicC
- TObjectPtr<UMaterialInstanceDynamic> _materialInstance
}
UCapsuleComponent <|-- UThermodynamicComponent
FOnTemperatureChanged <-- UThermodynamicComponent
FColorizer <.. UThermodynamicComponent
AThermodynamicActor --> UThermodynamicComponent
AThermodynamicActor ..> FOnTemperatureChanged
@enduml
Updated with the new level script class
@startuml
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam sequenceParticipant underline
skinparam linetype ortho
class UThermodynamicComponent {
+ UThermodynamicComponent(const FObjectInitializer& objectInitializer)
+ void TickComponent(float deltaTime, ELevelTick tickType, FActorComponentTickFunction* thisTickFunction) override
+ void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override
+ double GetTemperature() const
+ FOnTemperatureChanged OnTemperatureChanged
--
# double _heatCapacity
# double _initialTemperature
--
- void _setTemperature(double temperature)
- void _setHeatCapacity(double heatCapacity)
- double _getTemperatureDelta(const TArray<TObjectPtr<UPrimitiveComponent>>& overlappingComponents, float deltaTime)
- void _increaseInteractorsCount()
- void _setCurrentTempAsNext()
- double _currentTemperature
- double _nextTemperature
- static constexpr double ROD_CONSTANT
- uint32 _numOfInteractors
- uint32 _counterOfInteractors
}
class FOnTemperatureChanged << (D, orchid) >>
class FColorizer {
+ static FLinearColor GenerateColorFromTemperature(double temperature)
--
- static FLinearColor _interpolateInRGB(double value, double min, double max, FLinearColor colorMin, FLinearColor colorMax)
}
class AThermodynamicActor {
+ AThermodynamicActor()
--
# void BeginPlay() override
--
- void _updateMaterialBasedOnTemperature(double temperature)
- TObjectPtr<UStaticMeshComponent> _staticMesh
- TObjectPtr<UThermodynamicComponent> _thermodynamicC
- TObjectPtr<UMaterialInstanceDynamic> _materialInstance
}
class AThermodynamicLevelScript {
# void BeginPlay() override
# double _gridStep
# double _airTemperature
# double _airHeatCapacity
# int32 _moleculesPerCellSide
# TSubclassOf<AThermodynamicActor> _moleculeClass
--
- UEnvironmentGridWorldSubsystem::FGridSpawnAttributes _buildGridSpawnAttributes(TObjectPtr<ATriggerBox> gridTriggerBox)
- void _generateAir(double cellSide)
}
class UEnvironmentGridWorldSubsystem
struct FGridSpawnAttributes {
+ FVector2D GridCenter
+ double ZExtension
+ double Step
+ int32 NCellsX
+ int32 NCellsY
}
UCapsuleComponent <|-- UThermodynamicComponent
FOnTemperatureChanged <-- UThermodynamicComponent
FColorizer <.. UThermodynamicComponent
AThermodynamicActor --> UThermodynamicComponent
AThermodynamicActor ..> FOnTemperatureChanged
AThermodynamicLevelScript --> AThermodynamicActor
UEnvironmentGridWorldSubsystem <.. AThermodynamicLevelScript
UEnvironmentGridWorldSubsystem <.. FGridSpawnAttributes
@enduml
What?
The idea is to add a custom
UCapsuleComponent
,UThermodynamicComponent
, that stores adouble
value representing the temperature of the owner actor. When two instances of this class overlap, heat flows from the hottest body to the coldest one. This causes the temperature of the two bodies to change according to the following formula:_A description of how the formula was derived can be found in this document: Heat_transmission_formula.pdf._
To allow heat exchanges when components are not overlapping, ScalarField maps will hold a lattice of sphere-shaped static actors, each owning a
UThermodynamicComponent
, whose job is to simulate air.Why?
This is the core idea behind ScalarField gameplay: to have a semi-realistic simulation of heat exchanges in an RPG videogame of magic.