Skip to content

Commit

Permalink
[ENGINE] Separated last bits of mixed engine/game
Browse files Browse the repository at this point in the history
- integration test only refers to a generic gameapp
- define a specific game app for pico-sonic
- set/inject app/gamestate proxies on game side
- initialize gameapp correctly from each main entry point
  • Loading branch information
hsandt committed May 13, 2019
1 parent 87ca841 commit e35be0f
Show file tree
Hide file tree
Showing 14 changed files with 884 additions and 385 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ Alternatively, to edit the spritesheet in your favorite editor:
2. Edit it in your favorite editor
3. Import it back to PICO-8 with the PICO-8 command `import spritesheet.png`

## Development

### Documentation

Most of the documentation lies in code comment.

`<fun1, fun2>` means a duck-typed object that must implement functions named `fun1` and `fun2`

## New project

If you use the scripts of this project to create a new game, in order to use build command `p8tool: edit data` you need to create a pico8 file at data/data.p8 first. To do this, open PICO-8, type `save data`, then copy the boilerplate file to data/data.p8.
Expand Down
7 changes: 7 additions & 0 deletions src/engine/application/flow.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ require("engine/core/class")
local logging = require("engine/debug/logging")
--#endif

-- abstract gamestate singleton (no actual class, make your own as long as it has member/interface below)
-- type string gamestate type name
-- on_enter function() gamestate enter callback
-- on_exit function() gamestate exit callback
-- update function() gamestate update callback
-- render function() gamestate render callback

-- flow singleton
-- state vars
-- curr_state gamestates current gamestate
Expand Down
98 changes: 98 additions & 0 deletions src/engine/application/gameapp.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
local flow = require("engine/application/flow")
local class = require("engine/core/class")
local input = require("engine/input/input")

-- main class for the game, taking care of the overall init, update, render
-- usage: derive from gameapp and override on_start, on_reset, on_update, on_render
-- in the main _init, set the initial_gamestate and call the app start()
-- in the main _update(60), call the app update()
-- in the main _draw, call the app render()
-- in integration tests, call the app reset() before starting a new itest
local gameapp = new_class()

-- constructor: members are only config values for init_modules
-- managers {<start, update, render>} sequence of managers to update and render in the loop
-- initial_gamestate string|nil key of the initial first gamestate to enter (nil if unset)
-- set it manually before calling start(),
-- and make sure you called register_gamestates with a matching state
function gameapp:_init()
self.managers = {}
self.initial_gamestate = nil
end

-- register the managers you want to update and render
-- they may be managers provided by the engine like visual_logger and profiler,
-- or custom managers, as long as they provide the methods `update` and `render`
-- in this engine, we prefer injection to having a configuration with many flags
-- to enable/disable certain managers.
-- we can still override on_update/on_render for custom effects, but prefer handling managers when possible
function gameapp:register_managers(...)
for manager in all({...}) do
add(self.managers, manager)
end
end

-- override to add gamestates to flow singleton
function gameapp:register_gamestates()
-- ex:
-- flow:add_gamestate(...)
end

-- unlike _init, init_modules is called later, after finishing the configuration
-- in pico-8, it must be called in the global _init function
function gameapp:start()
self:register_gamestates()

-- REFACTOR: consider making flow a very generic manager, that knows the initial gamestate
-- and is only added if you want
assert(self.initial_gamestate ~= nil, "gameapp:start: gameapp.initial_gamestate is not set")
flow:query_gamestate_type(self.initial_gamestate)
for manager in all(self.managers) do
manager:start()
end
self:on_start()
end

-- override to initialize custom managers
function gameapp:on_start() -- virtual
end

--#if itest
function gameapp:reset()
flow:init()
self:on_reset()
end

-- override to call :init on your custom managers, or to reset anything set up in
-- in gameapp:start/on_start, really
function gameapp:on_reset() -- virtual
end
--#endif

function gameapp:update()
input:process_players_inputs()
for manager in all(self.managers) do
manager:update()
end
flow:update()
self:on_update()
end

-- override to add custom update behavior
function gameapp:on_update() -- virtual
end

function gameapp:draw()
cls()
flow:render()
for manager in all(self.managers) do
manager:render()
end
self:on_render()
end

-- override to add custom render behavior
function gameapp:on_render() -- virtual
end

return gameapp
55 changes: 36 additions & 19 deletions src/engine/test/integrationtest.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ require("engine/test/assertions")
--#if log
local logging = require("engine/debug/logging")
--#endif
-- engine -> game reference is not good, consider using flow directly
-- and isolating active gamestates somewhere else (e.g. a generic gameapp in engine)
local gameapp = require("game/application/gameapp")
local input = require("engine/input/input")

local mod = {}
Expand All @@ -21,7 +18,7 @@ test_states = {
}

-- integration test manager: registers all itests
-- itests {string: itest} registered itests, indexed by name
-- itests {string: itest} registered itests, indexed by name
itest_manager = singleton(function (self)
self.itests = {}
end)
Expand Down Expand Up @@ -118,7 +115,26 @@ function itest_manager:init_game_and_start_by_index(index)
end

-- integration test runner singleton
-- test lifetime:
-- usage:
-- first, make sure you have registered itests via the itest_manager
-- and that you are running an itest via itest_manager:init_game_and_start_by_index (a proxy for itest_runner:init_game_and_start)
-- in _init, create a game app, set its initial_gamestate and set itest_runner.app to this app instance
-- in _update(60), call itest_runner:update_game_and_test
-- in _draw, call itest_runner:draw_game_and_test

-- attributes
-- initialized bool true if it has already been initialized.
-- initialization is lazy and is only needed once
-- current_test integration_test current itest being run
-- current_frame int index of the current frame run
-- _last_trigger_frame int stored index of the frame where the last command trigger was received
-- _next_action_index int index of the next action to execute in the action list
-- current_state test_states stores if test has not started, is still running or has succeeded/failed
-- current_message string failure message, nil if test has not failed
-- app gameapp gameapp instance of the tested game
-- must be set directly with itest_runner.app = ...

-- a test's lifetime follows the phases:
-- none -> running -> success/failure/timeout (still alive, but not updated)
-- -> stopped when a another test starts running
itest_runner = singleton(function (self)
Expand All @@ -128,32 +144,36 @@ itest_runner = singleton(function (self)
self._last_trigger_frame = 0
self._next_action_index = 1
self.current_state = test_states.none
self.current_message = nil -- only defined when current_state is failure
self.current_message = nil
self.app = nil
end)

-- helper method to use in rendered itest _init
function itest_runner:init_game_and_start(test)
-- if there was a previous test, gameapp was already initialized,
-- so reset it now (we could also just keep it and change the gamestate
-- to void, if we are sure that all the itests have the same required modules)
assert(self.app ~= nil, "itest_runner:init_game_and_start: self.app is not set")

-- if there was a previous test, app was initialized too, so reset both now
-- (in reverse order of start)
if self.current_test then
gameapp.reinit_modules()
self:stop()
self.app:reset()
end

gameapp.init(test.active_gamestates)
self.app:start()
itest_runner:start(test)
end

-- helper method to use in rendered itest _update60
function itest_runner:update_game_and_test()
if self.current_state == test_states.running then
-- update gameapp, then test runner

-- update app, then test runner
-- updating test runner 2nd allows us to check the actual game state at final frame f,
-- after everything has been computed
-- time_trigger(0.) initial actions will still be applied before first frame
-- thanks to the initial _check_next_action on start, but setup is still recommended
log("frame #"..self.current_frame + 1, "trace")
gameapp.update()
self.app:update()
self:update()
if self.current_state ~= test_states.running then
log("itest '"..self.current_test.name.."' ended with "..self.current_state, "itest")
Expand All @@ -166,10 +186,11 @@ end

-- helper method to use in rendered itest _draw
function itest_runner:draw_game_and_test()
gameapp.draw()
self.app:draw()
self:draw()
end

-- start a test: integration_test
function itest_runner:start(test)
-- lazy initialization
if not self.initialized then
Expand All @@ -179,10 +200,6 @@ function itest_runner:start(test)
-- log after _initialize which sets up the logger
log("starting itest: "..test.name, "trace")

if self.current_test then
self:stop()
end

self.current_test = test
self.current_state = test_states.running

Expand Down Expand Up @@ -222,7 +239,7 @@ function itest_runner:draw()
api.print(self.current_test.name, 2, 2, colors.yellow)
api.print(self.current_state, 2, 9, self:_get_test_state_color(self.current_state))
else
api.print("no itest running", tuned("x", 8), tuned("y", 8), colors.white)
api.print("no itest running", 8, 8, colors.white)
end
end

Expand Down
Loading

0 comments on commit e35be0f

Please sign in to comment.