#what is "state data"? Also maybe we

1 messages · Page 1 of 1 (latest)

pliant juniper
#

"this furnace has cooked for 60 / 75 seconds"
"this crop has grown 4 / 10 days"

craggy tendon
#

no i meant this

#

I mean there are essentially two ways to do this

#

with a bunch of hybrid ways in between

#

One is basically polymorphism, and one is composition.

For polymorphism you basically have:

Dictionary<Vector2Int, TileData> tiles;

abstract class TileData {
  string name;
  // etc
}

class Chest : TileData {
  Inventory contents;
}

class Crop : TileData {
  int growthProgress;
  int growthLimit;
}```

For composition you basically have:

```cs
Dictionary<Vector2Int, TileData> tiles;

class TileData {
  List<Attribute> attributes;
}

struct Attribute {
  // choose enums or strings here
  string/AttributeType attributeName;

  double doubleVal;
  string stringVal;

  // helper functions and properties to deal with the different type possibility.
}```
pliant juniper
#

The latter seems more data efficient but also a bit harder to reason around

#

I guess my conception would be like a behavior layer that is stateless, and a data layer that has the data. Which may be an anti-pattern as it's more of a webapp framework.

like

struct helloStruct = { string message = "hello"}
helloStruct = UpdateDataBlock.DoUpdate(helloStruct, "world")

#

idk. this all makes my head spin

craggy tendon
#

yeah that makes sense

#

the nice thing is that if you use the "composition" approach, the specific behavior that cares about a specific data block just knows what data it needs by convention

#

so the lack of type safety isn't a huge concern

#

e.g.

DoCropUpdate(Datablock block) {
  int currentGrowth = block.GetInt("cropGrowth");
  // etc.
}```
pliant juniper
#

so I guess world update loop would be like
iterate through currently rendered chunks and blocks
if (Datablock.id == crop) updateCrop(Datablock)
or something like that

craggy tendon
#

Essentially - though you could get a bit more sophisiticated about the behavior dispatch

pliant juniper
#

like a polymorphic update method?

#

procedural world is a bit ambitious lol ugh

#

fun problem to solve, but hard

#

i wanna build scalable but also trying to build everything at once is overwhelming

craggy tendon
#

either polymorphism or using delegates. E.g.

delegate void BlockUpdater(DataBlock data);

Dictionary<BlockType, BlockUpdater> dispatchDict;

void Setup() {
  dispatchDict[BlockType.Crop] = CropUpdater;
  dispatchDict[BlockType.Chest] = ChestUpdater;
  // etc...
}

public BlockUpdater GetBehavior(BlockType bt) {
  return dispatchDict[bt];
}```
pliant juniper
#

reminds me of redux in js

#

it's annoying because I have a cs background and do software professionally. but web arch concepts don't map easily to pure OOP.

bright skiff
#

what are you building out of curiosity? is it a streamable world? how many concurrently simulated cells?

pliant juniper
#

There's more to it than that

bright skiff
#

no doubt but it's a good mental image

pliant juniper
#

The frustrating part is that it makes my head spin trying to reason about this all. like there are similarities to web arch in that

data/model layer - state/db
behavioral layer - API
view - UI / user input

#

but the separation is less clear in OOP

craggy tendon
#

MVP/MVC and OOP are sort of orthogonal concepts

pliant juniper
#

makes sense why my head is spinning lol

craggy tendon
#

OOP is just.. the way C# operates. You can build your MVP/MVC framework on top of C#'s OOP system. It's really not much different from how you'd do it in JS.

pliant juniper
#

Like my concept for the world is as follows
worldTileData ScriptObj

  • represents fixed data and behavior
    worldTilePureData primatives
  • data storage / persistence
    terrainManager
  • event handler / orchestrator
#

The separation of functionality and data seems like a requirement for procedural world

#

Usually it would be the same class

craggy tendon
#

absolutely. Not least of which because you will need to serialize this world presumably to save it between play sessions

#

having a well structured, holistic serializable data model of the current state of the world is a must

pliant juniper
#

my brain goes composition
IWaterSource has these data fields
IInteractable has these functionalities
etc
if those fields are variable though, how do I store the composed thing.
like if i did attribute struct list, wouldn't the struct need a static type?

#

sorry if I'm wasting yalls time

craggy tendon
#

That was the thing I had above:

class TileData {
  List<Attribute> attributes;
}

struct Attribute {
  // choose enums or strings here
  string/AttributeType attributeName;

  double doubleVal;
  string stringVal;

  // helper functions and properties to deal with the different type possibility.
}```
#

something like this

#

this is a simplified version

pliant juniper
#

How would this work for like cropdata vs chest data. for simplicity lets say cropdata is
int totalGrowData
int currDays

and chest is
int[] itemIds

#

(opening link now)

craggy tendon
#

for the chest I would probably just have one attribute and it would be like InventoryID - and this could just be a string or numerical ID pointing to separately stored data

#

I wouldn't store the actual inventory "inline" at this layer, if that makes sense

pliant juniper
#

ahh

#

basically like storing keys to db entries

#

it would allow me to query the data

craggy tendon
#

yeah- the inventory (chests, etc) is kind of a special case where the data is a bit too deep and structured to live directly at thhe "tiledata" layer

#

but for something like a crop where the whole state can be like one or two ints, you could store it directly with this structure

pliant juniper
#

doesn't that risk the structure getting flooded?

craggy tendon
#

can you elaborate on "flooded"?

pliant juniper
#

crop has
int growDays
int totalDays

craftingMachine1 has
int secondsProcessed
int totalSeconds

for every new type in this example there are 2+ fields
with 10 types we have 20 fields

craggy tendon
#

Well they're not really individual fields here

#

it's a List<Attribute>

#

which are, essentially, key/value pairs

pliant juniper
#

ah

craggy tendon
#

so any given item only has the attributes in its list that are actually required for that item

pliant juniper
#

and the other fields would be null so you don't store much data for them

#

or are struct fields optional?

#

like could
{
int test = 1
} fit in a
{
int test;
string test2
} struct?

craggy tendon
#

It's not clear to me if you're talkking about the Attribute struct here or the ItemData or whatever it is that contains the List<Attribute>

#

struct fields are not optional in C#

#

they will always exist - though if it's a string it may be null for example

pliant juniper
#

I think ultimately I need to read up on this further but I definitely have more of a starting place from this conversation. I appreciate both of your patience and helpfulness

bright skiff
#

maybe a view from the bottom up?

You have the following needs:
persistence of your data across sessions
runtime behaviour

and you need an architecture that consolidates both well

You have a runtime entity(be it monobehaviour, or whatever your heart desires), and a serializable representation of that entity for data persistence. You then need an interface that allows rapid conversion.

By having a list of attributes the way @craggy tendon recommended, essentially this removes a large part of that burden. You can save your cells to disk as a list of attribute mapped to a position, and then pass it back to your runtime representations of your entities/cells when loading regions/chunks.

If, depending on your performance needs, you can maintain this list of attributes as the driving data in your actual runtime, this is fantastic. It means you just eliminated all needs for per-entity custom serialization logic

pliant juniper
#

So would an Attribute in this case represent all data for a cell? like why would one cell require more than one attribute if an attribute has all the potential data fields?

craggy tendon
#

it's ONE datapoint

#

one key/value pair

#

growthprogress: 5

#

something like that is one attribute

pliant juniper
#

but wouldn't the Attribute create a struct with all the other fields?

craggy tendon
#

no

#

again

bright skiff
#

Think of every field you would normally expose in your runtime. This stores them, individually, in a generalized form

craggy tendon
#

attribute is:

struct Attribute {
  string key;
  float value;
}```
#

for example

#

And you just have a list of them

pliant juniper
#

ok. but that would only be a float?

craggy tendon
#

The actual fields here are:

        [JsonProperty]
        [SerializeField] 
        private AttributeType type;
        
        [JsonProperty]
        [SerializeField] 
        private double value;```
#

and througfh clever use of properties I'm able to encode a lot of different types in this double field including int float boolenums, etc

#

e.g. ```cs
public float FloatValue {
get => (float) value;
set => this.value = value;
}

    public bool BoolValue {
        get => value != 0;
        set => this.value = value ? 1 : 0;
    }```
pliant juniper
#

oh clever

craggy tendon
#

And even:

        // We do something very cheeky here - store the Vector2 in the double.
        public Vector2 Vector2Value {
            get {
                // Interpret the double as a 64-bit ulong
                ulong combinedBits = DoubleToUInt64(value);

                // Extract the high 32 bits and the low 32 bits
                uint firstBits = (uint)(combinedBits >> 32);       // High 32 bits
                uint secondBits = (uint)(combinedBits & 0xFFFFFFFF); // Low 32 bits

                // Convert the uint bits back to floats
                float first = UIntToFloat(firstBits);
                float second = UIntToFloat(secondBits);

                return new(first, second);
            }
            set {
                // Convert floats to uint using unsafe casting
                uint firstBits = FloatToUInt(value.x);
                uint secondBits = FloatToUInt(value.y);

                // Combine both uint bits into one 64-bit ulong
                ulong combinedBits = ((ulong)firstBits << 32) | secondBits;

                // Interpret the combined bits as a double
                this.value = UInt64ToDouble(combinedBits);
            }
        }```
bright skiff
#

was wondering why the double rather than a float

craggy tendon
#

If you want even more flexibility you could add additional fields here but they will have significant memory costs, since every single attribute will have it, even if unused

#

but a string field is possible as well

pliant juniper
#

i think double and string would be sufficient to cover most simple data types

#

anything more complicated like Inventory would have to be a lookup elsewhere

bright skiff
#

if your world isn't endless, you could even encode an index in the double that maps to a contiguous buffer of strings, so you can maintain the double as the primary data and derive the string

pliant juniper
#

my world is ideally endless

#

but I do finally understand the attribute system

#

and I'm now impressed lol

craggy tendon
#

I've basically been evolving this system over 8-10 years or so and using it on various projects to differing levels of success

pliant juniper
#

in theory strings can be encoded to doubles too right?

#

actually nah

#

too big

craggy tendon
#

yes - strings of a limited length at least

#

only like 8 cahracters or 4 characters

#

depending on the encoding

#

for ascii you could encode 8 characters

pliant juniper
#

a sequence of attributes to represent a string XD

#

reinvent char[]

bright skiff
#

you could use the double to index a list of strings or contents though, solving your chest problem via attributes

#

so now you end up serializing all chests contents together in a collection, and the relevant cell has an attribute "chestContent" or w/e which maps to an index

pliant juniper
#

I have a registry

#

idk if it's best practice but I love it lol

bright skiff
#

in performance intensive code, best practice often f's right off. Wouldn't overthink it, engineer in a way that is easy to reason about and maintain/extend

pliant juniper
#

yeah ultimately I have a heavy initial load, and then I can hit the registry for assets, SO, etc.

#

depending on the size though it may get unwieldy

bright skiff
#

heavy's relative. It's a function of how many regions/chunks you're loading simultaneously.

pliant juniper
#

I probably will need to learn addressables at some point

bright skiff
#

you said earlier you're limiting to what's being rendered. If that's the case, then I wouldn't think twice, this is nothing. Esp if the size of cells is similar to stardew

pliant juniper
#

well the registry stores like assets

bright skiff
#

I don't know much about your game, but if you have a runtime registry for your chest, then it should probably be limited to chests in active regions. Which means the worst, worst case is chestMaxContent*renderedCells

pliant juniper
#

true. but yeah I also might just accept that my initial approach for anything may not ~~work ~~ scale but I'll learn from mistakes lol

bright skiff
#

ah, such is life. Iterate, refine, iterate, refine and hopefully ship

#

Best of luck, sounds like a cool project

pliant juniper
#

This was my first pass. Art style is going to change though and I'm reworking a lot lol

bright skiff
#

That's ambitious! Nice. Good luck