Often times you want what are effectively strings but where the actual number of different string values might be limited. For example, object type names. There will probably only be a handful of them but the flexibility in code of referring to them by string is nice… rather than having to defined a bunch of constant ints somewhere and keep them all in sync.
Because the number of strings is finite, sending full strings over the wire and/or storing them in a database is wasteful. String index lets you convert them to/from unique integer IDs that are safely stored in the ES.
getStringId(“my string”, true)
…will return an integer ID for the string “my string” or create one if it doesn’t already have one. If you pass false then it will return -1 if the string doesn’t already exist.
getString(id)
…is the reverse of that for if you want to in-code see what the value is again (usually for debug or user interface output).
I should probably fill out the javadoc for those methods and add a section to the nascent zay-es wiki.
Yeah, when you are storing your entities in a database and have 6000 entities with ObjectType(“foo”)… you look to save space/time wherever you can.
It’s also nice for comparisons of components if you ever need to because int == int is cheaper than string.equals(string).
Furthermore, because it’s global if the names appear in other types of components then they get the same ID… and that does sometimes happen.
I use it for a bunch of stuff. Object types, action names, etc… even some stuff I don’t store in components but just want smaller network traffic.
Checked both RemoteStringIndex and MemStringIndex first one will return null the second one is not considering that boolean at all an will create if it could not find string.
RemoteStringIndex:
@Override
public int getStringId( String s, boolean add ) {
if( add == true ) {
throw new UnsupportedOperationException("Clients cannot create new string mappings.");
}
indexLock.readLock().lock();
try {
Integer result = idIndex.get(s);
// Note: misses are not cached... we'd have no way to detect
// if they were filled in later.
if( result != null ) {
// We're done.
return result;
}
} finally {
indexLock.readLock().unlock();
}
// Otherwise...
// Need to look it up... which means we need to
// grab the write lock
// Must release the read lock first
indexLock.writeLock().lock();
try {
// Check the result again because between unlock
// and lock something else might have requested the same
// string.
Integer result = idIndex.get(s);
if( result != null ) {
return result;
}
// Else ask remote
result = remote.getStringId(s);
if( result != null ) {
idIndex.put(s, result);
stringIndex.put(result, s);
}
return result;
} finally {
indexLock.writeLock().unlock();
}
}
MemStringIndex:
@Override
public int getStringId( String s, boolean add ) {
// See if it already exists first
lock.readLock().lock();
try {
Integer result = index.get(s);
if( result == null ) {
// Ok, so we need to convert to a write lock
lock.readLock().unlock();
lock.writeLock().lock();
try {
// Check one more time in case another thread beat us to
// it.
result = index.get(s);
if( result == null ) {
// we still need to create it
result = nextId++;
index.put(s, result);
strings.put(result, s);
}
} finally {
// Reverse the lock state... we don't
// technically need the read lock anymore but
// it makes the logic easier
lock.readLock().lock();
lock.writeLock().unlock();
}
}
return result;
} finally {
lock.readLock().unlock();
}
}
Why you are not considering boolean add in MemStringIndexgetStringId method.
Because occasionally, if ever so rarely, I make a mistake.
Edit:… though I wonder how the first one returns null since ultimately I thought it delegates to the MemStringIndex on the other end of the connection. Ah… because you cannot create strings at all in the first case, only read them.
Also i am curious to know why you are not using ConcurrentHashMap instead of ReentrantReadWriteLock for synchronization . Sorry if this question is noob
As i could understand ConcurrentHashMap itself uses ReentrantReadWriteLock in the behind.
Because I’m not just sticking things in a hashmap, I’m also generating a new ID. Those two things have to be synchronized together or it all falls apart.
So, given that I have to synchronize over creation and storing, I also need to synchronize over reading… and a read/write lock is best for that.
Thanks for all your helps.
I made a patch for considering boolean add and made a PR.
One more question:
Isn’t it better to change position of
if( add == true ) {
throw new UnsupportedOperationException("Clients cannot create new string mappings.");
}
from
@Override
public int getStringId(String s, boolean add) {
if (add == true) {
throw new UnsupportedOperationException("Clients cannot create new string mappings.");
}
indexLock.readLock().lock();
try {
Integer result = idIndex.get(s);
// Note: misses are not cached... we'd have no way to detect
// if they were filled in later.
if (result != null) {
// We're done.
return result;
}
} finally {
indexLock.readLock().unlock();
}
// Otherwise...
// Need to look it up... which means we need to
// grab the write lock
// Must release the read lock first
indexLock.writeLock().lock();
try {
// Check the result again because between unlock
// and lock something else might have requested the same
// string.
Integer result = idIndex.get(s);
if (result != null) {
return result;
}
// Else ask remote
result = remote.getStringId(s);
if (result != null) {
idIndex.put(s, result);
stringIndex.put(result, s);
}
return result;
} finally {
indexLock.writeLock().unlock();
}
}
to be like this :
@Override
public int getStringId(String s, boolean add) {
indexLock.readLock().lock();
try {
Integer result = idIndex.get(s);
// Note: misses are not cached... we'd have no way to detect
// if they were filled in later.
if (result != null) {
// We're done.
return result;
}
} finally {
indexLock.readLock().unlock();
}
// Otherwise...
// Need to look it up... which means we need to
// grab the write lock
// Must release the read lock first
indexLock.writeLock().lock();
try {
// Check the result again because between unlock
// and lock something else might have requested the same
// string.
Integer result = idIndex.get(s);
if (result != null) {
return result;
}
// Else ask remote
result = remote.getStringId(s);
if (result != null) {
idIndex.put(s, result);
stringIndex.put(result, s);
}
else if (add == true) {
throw new UnsupportedOperationException("Clients cannot create new string mappings.");
}
return result;
} finally {
indexLock.writeLock().unlock();
}
}
I mean it should throw exception after it could not find string id but if it found so simply just return that. What do you think ?
And hopefully my last question for this thread is :
What do you think about using enum class fields in components, I mean would you use enum directly in components or you prefer to send enum ordinal number ?
For example i have an Animation component which has a LoopMode enum . should i set it as an int id or just let it be an enum ?
I’m dabbling with some ideas much like this - but just so I’m clear: You have a client-side state that looks for the animation component and creates the animations?
Do you then discard the component entirely after starting the animation or what does the logic entail?
Yes, there is a state (system) which listen for Animation component and plays animation on entity.
Animation system just going to play animation on entity based on animation component it is given. Animation system is not going to change or discard animation component. It is my AI system which based on AI state will decide which animation to set on entity.
So for example when character goes to walk state AI will set new animation component for walk animation and when character goes to idle state AI system will set new animation component for idle animation.
And my AI decision making will be based on behavior trees using GDX-AI and will use jmesteer for steering behavior.
I have a lot to do yet …
No. If you pass a bad parameter you should know right away instead of some later time when your game is running in some strange edge case.
The reason I don’t personally use enums is because the database layer doesn’t support them.
The reason the database layer doesn’t support them is because the ordinals may change for arbitrary reasons and render previously saved data to be nonsense.
Plus in some cases they are tempting but end up centralizing something that is probably better left flexible.