This will be a short one, but it’s something you can easily apply to your own code when managing objects that must be indexable by an ID or be referenced in some downstream code.
Recently, one of our developers wrote an entity system in a Python game server that they purpose-built for a toy game, and a trick they used for automatically assigning each entity (and each subclass of entity) was roughly this:
import socket
class Entity(object):
__ENTITY_ID_IDX__ = 0
# arguments here are only to demonstrate that subclasses somehow must call Entity.__init__ (super) in their initializer
def __init__(self, a, b, c):
self.entity_id = Entity.__ENTITY_ID_IDX__
Entity.__ENTITY_ID_IDX__ += 1
self.a = a
self.b = b
self.c = c
# Our Player subclass
class PlayerEntity(Entity):
def __init__(self, name, *args):
super().__init__(*args)
self.name = name
self.x = 0
self.y = 0
self.hp = 100
def setPosition(self, x, y):
self.x = x
self.y = y
# And a usage example for our server
class Server(object):
def __init__(self):
self.connections: dict[int, socket.socket] = {}
self.entities: dict[int, Entity] = {}
def connectionMade(self, sock: socket.socket):
ent = PlayerEntity('PlayerName')
ent.conn = sock
self.connections[ent.entity_id] = ent
self.entities[ent.entity_id] = ent
# EntitySpawn will serialize to something like an integer to represent type, an integer to say packet size, and then the entity ID,
# name, and (x, y) position of the entity. On the client side, they will have their own dictionary of integer to Entity, and process
# that there by rendering it, doing client-side prediction, and especially referencing the ID in their own sent packets
self.broadcast(EntitySpawn(ent))
def broadcast(self, packet):
for conn in self.connections:
conn.send(packet.serialize())
def onAttack(self, playerEnt, targetEntityID):
# we received an onAttack packet (parsed from elsewhere, then given to this function)
# the target entity ID should be in our self.entities dictionary
if targetEntityID not in self.entities:
# for defensive programmings sake, we'll check
playerEnt.sock.shutdown()
return
tent = self.entities[targetEntityID]
tent.hp -= 1
# you can guess how this might be serialized
self.broadcast(PlayerHealth(entity_id=tent.entity_id, new_health=tent.hp))
Understanding the code
We used a lot of psuedo code in this one, so focus on learning the concept rather than immediately rushing to use the code.
We have our Entity class, which has a class-level variable called __ENTITY_ID_IDX__
. Every time the constructor (__init__
function) of the class is called (especially when subclasses are initialized!) this value gets incremented.
In this example, it means that every time a player connects and we make a new PlayerEntity object, they get assigned their own ID that never gets re-used and can be broadcast without any special handling directly to other connected players.
Broadly speaking, the purpose here can be boiled down to “do as little as possible to allow downstream consumers to reference our objects conveniently”. There’s no need to manually set entity IDs when they’re set automatically.
Not much else to this one, be sure to subscribe to our newsletter to get more programming tips!