1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-29 10:35:23 +00:00
1 ECS: Scene Systems
gravestench edited this page 2020-12-15 21:59:50 -08:00

What is a Scene system?

A Scene is an abstraction of akara.System, a special system that contains a scene graph and one (or many) rendering viewports. Scenes manage their own list of entities and render those entities to the viewports. Please, refer to d2systems.BaseScene.

A Scene system is a special type of system that has dependencies on several other systems. In particular, all scene systems require the following systems be added to the world in order for the scene to initialize:

  • d2systems.RenderSystem - responsible for initializing the renderer
  • d2systems.InputSystem - responsible for handling keyboard/mouse/gamepad input
  • d2systems.GameObjectFactory - responsible for creating game objects (which may be composites of many components)

The GameObjectFactory deserves special attention. At the time of writing, this is what it looks like in full:

type GameObjectFactory struct {
	akara.BaseSystem
	*d2util.Logger
	Sprites *SpriteFactory
	Shapes *ShapeSystem
	UI *UIWidgetSystem
}

func (t *GameObjectFactory) Init(world *akara.World) {
	t.World = world

	t.setupLogger()

	t.Debug("initializing ...")

	t.injectSubSystems()
}

func (t *GameObjectFactory) setupLogger() {
	t.Logger = d2util.NewLogger()
	t.SetPrefix(logPrefixGameObjectFactory)
}

func (t *GameObjectFactory) injectSubSystems() {
	t.Sprites = NewSpriteFactory(t.BaseSystem, t.Logger)
	t.Shapes = NewShapeSystem(t.BaseSystem, t.Logger)
	t.UI = NewUIWidgetFactory(t.BaseSystem, t.Logger, t.Sprites, t.Shapes)
}

func (t *GameObjectFactory) Update() {
	t.Sprites.Update()
	t.Shapes.Update()
	t.UI.Update()
}

As you can see, the GameObjectFactory is really just a wrapper for several other subordinate systems, like the SpriteFactory. The SpriteFactory is like any other system, except it provides these two very important methods:

func (t *SpriteFactory) Sprite(x, y float64, imgPath, palPath string) akara.EID
func (t *SpriteFactory) SegmentedSprite(x, y float64, imgPath, palPath string, xseg, yseg, frame int) akara.EID

Without going into too much gory detail, it should be easy to see that these methods are responsible for creating Sprites and SegmentedSprites. This leads into the next section...

How does a scene know which entities it owns?

each scene creates a local sceneObjectFactory. This literally has all of the same method signatures as the global GameObjectFactory system, but this local one handles adding entities to the scene's render list.

type sceneObjectFactory struct {
	*BaseScene
	*d2util.Logger
}
func (s *sceneObjectFactory) Sprite(x, y float64, imgPath, palPath string) akara.EID {
	s.Debugf("creating sprite: %s, %s", filepath.Base(imgPath), palPath)

	eid := s.sceneSystems.Sprites.Sprite(x, y, imgPath, palPath)
	s.SceneObjects = append(s.SceneObjects, eid)

	s.addBasicComponents(eid)

	return eid
}

The sceneObjectFactory is embedded in the BaseScene with the field name Add:

eid := scene.Add.Sprite(0, 0, spritePath, spritePalette)

How do i create a scene?

First of all, you need to embed a *d2systems.BaseScene inside of your scene struct:

type TestScene struct {
	*BaseScene
	booted        bool
}

Next, you need to make sure to handle BaseScene boot and update inside of the scene Update method:

func (s *TestScene) boot() {
	if !s.BaseScene.booted {
		s.BaseScene.boot()
		return
	}

	// do scene bootup stuff here...

	s.booted = true
}

func (s *TestScene) Update() {
	for _, id := range s.Viewports {
		s.Components.Priority.Add(id).Priority = scenePriorityTestScene
	}

	if s.Paused() {
		return
	}

	if !s.booted {
		s.boot()
		return
	}

	// add actual scene logic here 

	s.BaseScene.Update()
}

Optionally, you may want to make a provider function for creating an instance of your scene:

func NewTestScene() *TestScene {
	scene := &TestScene{
		BaseScene: d2systems.NewBaseScene("Test Scene"),
	}

	return scene
}

CAVEATS

At the time of writing, we need to manually manage the following:

  • the render priority of viewports (in relation to other scenes!)
  • We need to boot the BaseScene and return early in our scene's Update method if the base scene has not booted!
  • We need to manually call BaseScene.Update at the end of our scene's Update method
func (s *MouseCursorScene) Update() {
	for _, id := range s.Viewports {
		s.Components.Priority.Add(id).Priority = scenePriorityMouseCursor
	}

	if s.Paused() {
		return
	}

	if !s.booted {
		s.boot()
		return
	}

	s.updateCursorTransform()
	s.handleCursorFade()

	s.BaseScene.Update()
}

func (s *MouseCursorScene) boot() {
	if !s.BaseScene.booted {
		s.BaseScene.boot()
		return
	}

	s.registerTerminalCommands()

	s.createMouseCursor()

	s.booted = true
}

IT WOULD BE REALLY COOL IF WE DIDN'T HAVE TO DO THIS, PLEASE HELP FIX THIS!