Class problem - double-side access

When rapidly prototyping, it can be tempting to ignore design patterns and allow anti-patterns in the interest of development time. I think that with a little bit of care, you can still design quickly, but avoid the traps and pitfalls of late refactoring as a consequence of inter-dependencies and initial design assumptions that later often change.

There are some times when the child object must be made aware of the parent object. For example, it makes sense that a GameObject should be aware of the scene which contains it, and the scene the game world. Keeping object relationships at a depth of one-level is generally good design practice. However, if you can design objects which do not need their parents, and can be self-contained, you eliminate a large dependency upon the parent’s semantics, which is preferable.

With the one-level-relationship, you can quickly fall into the trap of doing something like self.parent.parent.some_attribute lookups, which create two-levels of dependency: the parent must have a “parent” attribute, and that parent object must have an attribute called “some_attribute”. Again, this can be a useful design choice - if you have game objects, the general world-scene-object structure will be fundamental to the system, and is very unlikely to change. You could provide the world and the scene to the object directly, but then you risk cluttering the namespace, and you’ve still assumed a basic world/scene relationship anyway, so the benefit is probably lost.

Of course, there are many ways in which objects can be created and configured, and these different solutions usually provide a clearer method than just passing a God object / performing multiple-depth lookups. Factories, component systems and message passing are just a few examples of how to reduce dependencies between objects.

Finally, with reference to your weapon example - the bullet really shouldn’t be doing that sort of work. Neither should the weapon - it’s a dumb tool:


class WeaponCantFire(Exception):
	pass
	


class WeaponAmmoEmpty(WeaponCantFire):
	pass
	
	
class MagazineEmpty(WeaponAmmoEmpty):
	pass


	
class WeaponNoMagazine(WeaponAmmoEmpty):
	pass	






class Magazine:
	
	def __init__(self, bullets):
		self._bullets = bullets
	
	@property
	def bullets(self):
		return self._bullets
	
	def eject_bullet(self):
		if not self._bullets:
			raise MagazineEmpty()
			
		self._bullets -= 1
		
	
class Weapon:


	def __init__(self, obj):
		self.magazine = None
		self.obj = obj
		self.max_range = 10
		
	def load_magazine(self, magazine):
		self.magazine = magazine
	
	def fire(self)
		magazine = self.magazine
		
		if magazine is None:
			raise WeaponNoMagazine()
			
		magazine.eject_bullet()
		
		target = obj.worldPosition + obj.getAxisVect((0, 1, 0)) * self.max_range
		ray = obj.rayCastTo(target)
		
		if not ray:
			return None
		
		some_damage = ... # Calculate from ray distance
		
		return dict(target=ray, damage=some_damage])
		


class Team:


	def __init__(self):
		self.score = 0
		self._damage_constant = 0.1
		
	def on_hit_enemy(self, enemy, damage):
		self.score += damage * self._damage_constant
	
	def on_hit_friendly(self, enemy, damage):
		self.score -= damage * self._damage_constant
	


class GameScenario:


	def on_hit_target(self, instigator, target, damage):
		if instigator.team == target.team:
			instigator.team.on_hit_friendly(target, damage)
			
		else:
			instigator.team.on_hit_enemy(target, damage)
			target.on_damaged(instigator, damage)
		
		
class Game:


	def __init__(self, scenario):
		self.scenario = scenario
		


class WeaponUser:


	def __init__(self, game, team):
		self.weapon = Weapon(self.obj.children['weapon'])
		self.game = game
		self.team = team
		
	def attack(self):
		hit_result = self.weapon.fire() # Raise some kind of WeaponCantFire exception if this fails!
		
		if hit_result:
			damage = hit_result['damage']
			target = hit_result['target']
			
			self.game.scenario.on_hit_target(self, target, damage)

This quick mock up gives, I imagine, a good starting point for a game. If you change how teams deal with damage (e.g allow friendly fire) or how damage is dealt (e.g multipliers for teams, powerups etc) this can be handled by the Team and Scenario classes. You could swap these out during the game to change the behaviour. You can change the functionality of the Weapon without any functional difference in the WeaponUser as long is it still has a fire method, and raises a useful exception.

I don’t necessarily advise using exceptions here, but I think it illustrates some general points about organisation and hierarchy.