Looking for high-level advice about program organization

I’m building a space 4X game that has animated 2D solar systems (as seen from above). I’ve got all the orbit math (well, v1), and I’ve even got a static render working, but I’m sort of puzzled by the problem of flushing the system state to Phaser’s Scene.update method to make it animate over time, and it occurs to me that I just don’t have good intuitions for how to organize responsibilities in a game.

For example: if I want the planets to be clickable, it seems they must be GameObjects (i.e. they must extend Phaser.GameObjects.GameObject). Well, every GameObject must be initialized with a reference to the owning Scene. That seems undesirable because it means my tree of abstract data-management classes (SolarSystem, Star, Planet, etc) must now become tightly coupled to Scenes. It means I can’t initialize any of those objects without marrying each one to a Scene, which will impact unit testing as well as the simulation of instances that are not being drawn (e.g. other planets that are very distant and that I have no intention of rendering).

From the other perspective, it seems like I want to re-use the same set of Shapes when redrawing these objects, and that it would be inefficient to do something like:

update( sceneAgeMs, deltaMs ) {
	// STEP 1: acquire a list of coordinates and sizes from objects being simulated
	// this is just a collection of scalars, it does not include any Phaser objects
	let drawData = stellarObjects.reduce(( geometryData, thisObject ) => {
		// tell each object to simulate the passage of time
		thisObject.tick(deltaMs / MS_PER_GAME_TICK)
		// collect its geometry data
		return geometryData.concat(thisObject.getDrawData())
	}, [])
	
	this.graphics.clear()
	// STEP 2: create new Shapes and draw each as specified
	drawData.forEach(shapeInfo => {
		let shape = new Phaser.Geom[shapeInfo.shapeName](...shapeInfo.shapeGeom)
		graphics.lineStyle(...shapeInfo.lineStyle)
		graphics.fillStyle(...shapeInfo.fillStyle)
		// stroke & fill the shape
		shape[shapeInfo.strokeMethodName](shape)
		shape[shapeInfo.fillMethodName](shape)
	})
}

How do folks normally do this stuff? Do they put all the “data simulation” state & logic into classes that extend GameObject, and then have each instance manage its own Shape instances which it then provides to methods that desire the shapes? Or do they have the scene track the set of Shapes and their “styling,” and use IDs (or pass-by-reference) to associate a bunch of pure state containers with the scene’s long-lived drawables?

I’m sorry if this is a lot to throw at y’all. I appreciate any advice.

One thing you could try is introduce “wrapper” Game Objects that are linked to your data classes. You’ll be able to access the data objects from the game’s code (and, for example, forward events to them) without having all of the features of a Game Object hanging off of them.

For what’s its worth, I’d recommend against using a Graphics object for anything substantial. Graphics objects are good for prototyping and creating textures at runtime, but they are completely rerendered every frame, which can quickly add up. You can instead use the Shape Game Objects you get from methods like this.add.rectangle. They cache their vertex data and batch just as well as a Graphics object. If you decide to try my suggestion about the wrapper objects, you could make the wrappers themselves be shape objects (even a using the low-level Shape object if you can figure out its API) and you wouldn’t need to render the scene yourself.

Side note: I also wouldn’t recommend using reduce in the way you do. The concat at the end of each iteration will create a new array every time, then throw it away on the next iteration. This is unnecessary overhead considering you can accomplish the same with map.

1 Like

Geom objects aren’t linked to scenes so you can use those all you like.

I would try something like this, if possible:

class SolarSystem {
  update (delta) {}

  render (graphics) {}
}

class Scene {
  create () {
    this.graphics = this.add.graphics();
  }

  update (time, delta) {
    solarSystem.update(delta);
    solarSystem.render(this.graphics);
  }
}
1 Like

This seems like good advice. I am curious whether this is what people generally do, or whether they collapse the data and GameObject into a single class. As an experiment (I’m still learning Phaser), I went ahead and refactored it all into straight GameObjects, and it works. But I’d still prefer to use common patterns.


Thanks very much for that tip about Geom objects. My refactor still uses them, but once I finish proving-out the linkage between data-over-time to rendering successive frames, I will revisit all the drawing stuff and switch to “Shape Game Objects.” This goes on the roadmap for sure.

One thing I really like about your suggestion is that it separates the update-state-over-time action from the draw-UI-from-state action. I’m planning this as an ARG with some offline behavior, and I think that will pretty much require that I be able to “scrub” through time independent from the render schedule.

Per @Telinc1’s advice, I think I should avoid using Geoms, though. It’s what I’m using right now, but I will eventually care about performance.

What about a bit of MVC pattern. So you.can have your model with data and state, and your View objects are phaser game objects that response to state changes.