PedrelliLuca / ScalarField

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

The game manages "Air Cell" actors based on their distance from the player's character #2

Closed PedrelliLuca closed 2 years ago

PedrelliLuca commented 2 years ago

What?

Add a system such that, based on the player character's location, the game:

Think of the map as a 2D grid. If the player character is standing in a given cell, then that cell and the 8 surrounding it are also active. The ones beyond can either be non-existent (they haven't been spawned yet) or cached (they have been spawned, but the player moved away from them and now they're not among the surrounding 8 anymore).

cells_management Cells' management. Pink: character. Orange: active cell. Blue: non-existing cell. Green: cached/inactive cell.

Why?

This system should help performance-wise. In my first iteration of the ScalarField prototype, a couple of years ago, I didn't develop this system: I had a single enormous air cell with a huge amount of "air molecule" actors. On a big map, that meant having hundreds of thousands of elements ticking in the scene to exchange heat, which caused the FPS to be extremely low.
By activating only the molecules closest to the player, the number of actors ticking simultaneously should become acceptable. By caching temperature-related data, I'll just need to reload such data and reactivate the cell if the player decides to revisit it.

PedrelliLuca commented 2 years ago

Grid system architecture - first draft

image

@startuml

skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam sequenceParticipant underline
skinparam linetype ortho

class UWorldSubsystem

struct FCellCoordinates {
  + bool operator==(const FCellCoordinates& other)
}

class UEnvironmentGridWorldSubsystem {
  + void PostInitialize() override
  + void OnCellEndOverlap(...)
  --
  - TMap<FCellCoordinates, TSet<FCellCoordinates>> _adjacencyList
  - TMap<FCellCoordinates, TObjectPtr<AEnvironmentCell>> _cells
}

class AEnvironmentCell {
  + bool UnfreezeInTime()
  + bool FreezeInTime()
  + TWeakObjectPtr<UBoxComponent> GetBoxComponent()
  + const FCellCoordinates& GetCoordinates() const
  + void SetCoordinates(FCellCoordinates coordinates)
  --

  - FCellCoordinates _coordinates
  - TObjectPtr<UBoxComponent> _boxC
}

class FEnvironmentCellCache

UWorldSubsystem <|-- UEnvironmentGridWorldSubsystem 
UEnvironmentGridWorldSubsystem o-- AEnvironmentCell
AEnvironmentCell --> FCellCoordinates
UEnvironmentGridWorldSubsystem o-- FCellCoordinates
@enduml
PedrelliLuca commented 2 years ago

After working on the environment system for a bit and better understanding the complexity of the problem, I came up with the following class diagram:

image

@startuml

skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam sequenceParticipant underline
skinparam linetype ortho

class UWorldSubsystem

struct FCellCoordinates {
  + bool operator==(const FCellCoordinates& other) const
  + bool operator!=(const FCellCoordinates& other) const
  + operator FVector2D() const
  + FString ToString() const
  + int32 X
  + int32 Y
}

class UEnvironmentGridWorldSubsystem {
  + void SpawnGrid()
  + void ActivateOverlappedCells(const TSet<AEnvironmentCell*>& cellsToActivate)
  --
  - void _spawnCellAtCoordinates(FCellCoordinates coordinates, double cellSide)
  - void _onCellEntered(FCellCoordinates cellEntered)
  - void _onCellLeft(FCellCoordinates cellLeft)
  - TMap<FCellCoordinates, TSet<FCellCoordinates>> _adjacencyList
  - TMap<FCellCoordinates, TObjectPtr<AEnvironmentCell>> _cells
  - TSet<FCellCoordinates> _overlappedCells

}

class AEnvironmentCell {
  + void FreezeTime()
  + void UnfreezeTime()
  + bool IsFrozen() const
  + double GetSide() const 
  + TWeakObjectPtr<UBoxComponent> GetBox()
  + void SetCoordinates(FCellCoordinates coordinates)
  + const FCellCoordinates& GetCoordinates() const
  + FOnCellBeginningOverlap OnCellBeginningOverlap
  + FOnCellEndingOverlap OnCellEndingOverlap
  --
  - void _onCellBeginningOverlap(...)
  - void _onCellEndingOverlap(...)
  - double _side
  - bool _isFrozen
  - FCellCoordinates _coordinates
  - FDateTime _freezingTime
  - TObjectPtr<UBoxComponent> _boxC
  - TObjectPtr<UStaticMeshComponent> _staticMeshC
  - TObjectPtr<UMaterialInstanceDynamic> _materialInstance
}

class AScalarFieldCharacter {
  # void BeginPlay() override
}

class UEnvironmentGridSettings
class UEnvironmentCellSettings

class FOnCellBeginningOverlap << (D, orchid) >>
class FOnCellEndingOverlap << (D, orchid) >>

UWorldSubsystem <|-- UEnvironmentGridWorldSubsystem 
UEnvironmentGridWorldSubsystem o-- AEnvironmentCell

AEnvironmentCell --> FCellCoordinates
UEnvironmentGridWorldSubsystem --> FCellCoordinates

FOnCellBeginningOverlap <-- AEnvironmentCell
FOnCellEndingOverlap <-- AEnvironmentCell
UEnvironmentGridWorldSubsystem ..> FOnCellBeginningOverlap  
UEnvironmentGridWorldSubsystem ..> FOnCellEndingOverlap  

AScalarFieldCharacter ..> UEnvironmentGridWorldSubsystem
AScalarFieldCharacter <.. AEnvironmentCell

UEnvironmentGridSettings <.. UEnvironmentGridWorldSubsystem 
UEnvironmentCellSettings <.. AEnvironmentCell
UEnvironmentCellSettings <.. UEnvironmentGridWorldSubsystem 

@enduml
PedrelliLuca commented 2 years ago

With this commit I managed to make AEnvironmentCell independent from AScalarFieldCharacter. This allowed me to create a grid-related classes module, EnvironmentGrid that knows nothing about the ScalarField module, improving the architecture.

image

@startuml
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam sequenceParticipant underline
skinparam linetype ortho

package ScalarField {
class AScalarFieldCharacter {
  # void BeginPlay() override
}
}

package EnvironmentGrid {
struct FCellCoordinates {
  + bool operator==(const FCellCoordinates& other) const
  + bool operator!=(const FCellCoordinates& other) const
  + operator FVector2D() const
  + FString ToString() const
  + int32 X
  + int32 Y
}

class UEnvironmentGridWorldSubsystem {
  + void SpawnGrid()
  + void ActivateOverlappedCells(const TSet<AEnvironmentCell*>& cellsToActivate)
  --
  - void _spawnCellAtCoordinates(FCellCoordinates coordinates, double cellSide)
  - void _onCellEntered(FCellCoordinates cellEntered)
  - void _onCellLeft(FCellCoordinates cellLeft)
  - TMap<FCellCoordinates, TSet<FCellCoordinates>> _adjacencyList
  - TMap<FCellCoordinates, TObjectPtr<AEnvironmentCell>> _cells
  - TSet<FCellCoordinates> _overlappedCells

}

class AEnvironmentCell {
  + void FreezeTime()
  + void UnfreezeTime()
  + bool IsFrozen() const
  + double GetSide() const 
  + TWeakObjectPtr<UBoxComponent> GetBox()
  + void SetCoordinates(FCellCoordinates coordinates)
  + const FCellCoordinates& GetCoordinates() const
  + FOnCellBeginningOverlap OnCellBeginningOverlap
  + FOnCellEndingOverlap OnCellEndingOverlap
  --
  - void _onCellBeginningOverlap(...)
  - void _onCellEndingOverlap(...)
  - double _side
  - bool _isFrozen
  - FCellCoordinates _coordinates
  - FDateTime _freezingTime
  - TObjectPtr<UBoxComponent> _boxC
  - TObjectPtr<UStaticMeshComponent> _staticMeshC
  - TObjectPtr<UMaterialInstanceDynamic> _materialInstance
}

class UEnvironmentGridSettings
class UEnvironmentCellSettings

class FOnCellBeginningOverlap << (D, orchid) >>
class FOnCellEndingOverlap << (D, orchid) >>

}

UEnvironmentGridWorldSubsystem o-- AEnvironmentCell

AEnvironmentCell --> FCellCoordinates
UEnvironmentGridWorldSubsystem --> FCellCoordinates

FOnCellBeginningOverlap <-- AEnvironmentCell
FOnCellEndingOverlap <-- AEnvironmentCell
UEnvironmentGridWorldSubsystem ..> FOnCellBeginningOverlap  
UEnvironmentGridWorldSubsystem ..> FOnCellEndingOverlap  

UEnvironmentGridWorldSubsystem <.. AScalarFieldCharacter

UEnvironmentGridSettings <.. UEnvironmentGridWorldSubsystem 
UEnvironmentCellSettings <.. AEnvironmentCell
UEnvironmentCellSettings <.. UEnvironmentGridWorldSubsystem

@enduml