Python Tip: Global, auto-incrementing IDs for class instances WITHOUT module-scope variables

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!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top