PedrelliLuca / ScalarField

A TopDown RPG Game, where magic works by interacting with the environment.
4 stars 0 forks source link

Concept of temperature and heat exchanges #4

Closed PedrelliLuca closed 2 years ago

PedrelliLuca commented 2 years ago

What?

The idea is to add a custom UCapsuleComponent, UThermodynamicComponent, that stores a double 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:

image _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.

PedrelliLuca commented 2 years ago

Old ScalarFiled interaction

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:

image The first interaction that occurs is A-B, and the second is B-C

Non-simultaneous interactions

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.

Non-deterministic output

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.

New ScalarField: storing two temperatures

The problems above can be fixed by taking the following actions:

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.

When do we execute 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).

PedrelliLuca commented 2 years ago

Here's a first draft of the UML:

image

@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
PedrelliLuca commented 2 years ago

Below is a UML update

image

@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
PedrelliLuca commented 2 years ago

Updated with the new level script class

uml

@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