NateTheGreatt/bitECS

Storing strings

Closed this issue · 12 comments

As far as I understand there is no way to store anything other than numbers in components.
I understand that this is part of how BitECS manages to get such fantastic performance.
When using BitECS, what is the expectation for how to attach a string to an entity?

I have come up with at least one strategy to handle for this, using the TextEncoder API.

import {defineComponent, Types, createWorld, addEntity, addComponent} from 'bitecs'

const Name = defineComponent({
    value: [Types.ui8, 12],
})
const world = createWorld()
const bob = addEntity(world)
addComponent(world, Name, bob)
Name.value[bob] = new TextEncoder().encode('Robert')
console.log(new TextDecoder().decode(Name.value[bob]))

I haven't benchmarked this approach, but my gut says this approach wouldn't be very performant.

yep! text encoder is one way to do it, but indeed slow.

using some kind of integer->string mapping would be the most performant:

const enum = ['a','b','c']
const c = defineComponent({ someString: Types.ui8 })
addComponent(world, c, eid)
c.someString[eid] = enum.indexOf('b')
const value = enum[c.someString[eid]] // => 'b'

Probably no slower than Object based ECS's that support string types tbh.

In the case of storing strings that are not reused by multiple components, which I think is by no means an edge case, an integer->string mapping would be slower than just using an array as a BitECS type, right?

| storing strings that are not reused by multiple components
not sure what you mean exactly. you can have a mapping per-component if need be.

it really depends on what the exact use-case is. if the strings do not change often, or if you need to send the strings over the wire, then a mapping will be the most performant. if the strings are changing a lot or have an extreme number of permutations then i would recommend keeping that data outside of the ECS component data.

I don't see the performance gains of using a mapping, isn't it just kicking the performance bottleneck can down the road?
Updating a string in an array of strings is slow. Updating a string in a map stored outside the ECS is the same as updating a string stored directly in the ECS but with more steps, right?

Do you have an example of a string you would want to store in a component?

Looking up a value from a map is really fast and really the enum is just a convenience for readable code, you could just use integers.

If you need to store arbitrary strings in the ECS, you probably want to actually redesign that feature. For instance, for player names, I keep a map of entity id to player name and keep the client up to date using event based messages (which are TextEncoder encoded).

I've made feature complete games without the need for storing arbitrary strings in the ECS. Curious what you would want to store as a string, maybe I can help you redesign the feature/ figure out the most performant way to achieve your goal.

I'm surprised that it isn't an obvious necessity. I'm curious if I'm misunderstanding something about how to effectively apply an ECS architecture.
I'm switching over from ECSY, which implements strings as a possible type. In a number of cases, theres an easy fix by changing from using a string to a tag. For example, I had a component used by my rendering system, "Primitive", with a field "shape". Before, I'd use a string like "sphere" to have the renderer create a sphere, but switching this to a sphere tag component is just as easy and I'm finding it cleaner overall.
Some components that will need changed include a "CurrentAnimation" component that is used to set the current animation of an NPC. In this case, I can see where switching the way I store my animations from key->value to an ordered list, and then setting the current animation by Integer will be extremely performant. It'll add a little bit more code complexity and difficulty reasoning about, but worth it for the performance improvements.

Where I don't yet have any solution in mind is for names.

So shape is definitely finite.

export enum Shape {
  Box,
  Sphere
}

const Collider = defineComponent({ shape: 'i8' })
const eid = addEntity()
addComponent(Collider, eid)
Collider.shape[eid] = Shape.Box

Same can be said for animations. Finite. This is going to be much more performant than storing strings.

For things like chat and player names, see my last comment. They should be in sub systems outside of the ecs that can be referenced by the ecs if needed.

I'm still not quite on board with understanding the performance benefits of the string map setup, but thats fine. I'll play with it and see if I can figure out what I'm missing.

Thank you both for taking the time to answer my questions. I really appreciate it!

Sure no problem! Once we finish up our networking tool we will have some concrete examples of how we solve these problems that we can share.