In puzzle games, it's important to consider alternate solutions. It's especially important in games where some moves cannot be reversed, such as pushing a box into a corner. We'll use the game history we've accumulated to step backward and reverse our most recent move when the user shakes the device.
Our design says that the user should be able to undo their last move by shaking the device.
Shaking is recognized by looking at the accelerometer
events. These events are dispatched only to the global Runtime
target; our UI will need to listen to this target, and stop listening when its scene is not active, but ideally we don't want the game scene to know that a submodule needs to be connected to Runtime
or disconnected later. Perform the following steps to recognize the Undo
requests:
createScene
event, the interface can register and unregister itself by listening to the Scene
object for the enterScene
and exitScene
events. We'll add the functions to listen or stop listening to Runtime
first, in interface.lua
:local function engage(self, event) Runtime:addEventListener('accelerometer', self) end local function disengage(self, event) Runtime:removeEventListener('accelerometer', self) end return function(game)
target:addEventListener('tap', self) self.enterScene = engage game:addEventListener('enterScene', self) self.exitScene = disengage game:addEventListener('exitScene', self) local counter = display.newText(self, "0", 10, 10, native.systemFont, 24)
accelerometer
events, it needs an appropriate method:game:addEventListener('exitScene', self) function self:accelerometer(event) if event.isShake then self:dispatchEvent{name = 'Move'; action = 'undo'} end end local counter = display.newText(self, "0", 10, 10, native.systemFont, 24)
Fortunately, we don't have to do any complex tracking of accelerometer
data to recognize whether the user is shaking the device; Corona uses routines provided by the host OS to determine that for us.
So now the interface can dispatch the undo
events for Move
when the device is shaken. The Game
object is already listening for the Move
events on the interface, so we need to add some recognition to the existing routine. Open game.lua
and find the scene:Move
function.
if move.action == 'push' and checkWin(self.Map) then
self:dispatchEvent{name = 'Game'; action = 'stop', state = self.Map, Game = self}
end
elseif event.action == 'undo' then
end
end
The Move
function currently only acts when the requested action is an attempt to move. When that's not the case, we can now check whether the Move
was an undo
request, instead.
The question then becomes how to restore the game back to its state before a given move. Fortunately, each Move
event on the game that's either a push
or step
event contains the positions of the moving elements before the move was completed. So we can actually save these events themselves as a way of tracking the game history (specifically, the push
events, since player steps don't meaningfully advance the game). Perform the following steps for saving move history and backing moves out:
push
events have taken place. But since we're going to be saving them in chronological order in an array, we can use the length of that array to know how many moves have taken place instead. Find the scene:enterScene
code in game.lua
and change the following line: self.Map = require "map" (sok.load(event.params.File, event.params.Level))
self.MoveCount = 0
self.World:Load(self.Map)
So that it reads the following:
self.Map = require "map" (sok.load(event.params.File, event.params.Level))
self.History = {}
self.World:Load(self.Map)
scene:Move
that formerly relied on MoveCount
to this new system:if move.action == 'push' then table.insert(self.History, move) end move.count = #self.History self:dispatchEvent(move)
Move
event in the history as a way of resetting the Move
action:elseif event.action == 'undo' then local lastMove = table.remove(self.History) if lastMove then end end
The if
block just ensures that we don't attempt to undo
the beginning of the game.
step
events in the history, we can't guarantee that the player is still in the position it was in immediately after the move being undone. Although the box pushed will be in the same position as it was in immediately after the move being undone, since this is the most recent push. So we get the player's current position from the map, clear that position, and set the player's position before the move being undone as his/her current position:if lastMove then local column, row = self.Map:FindPlayer() local realm = self.Map.Moving realm[row][column] = nil realm[lastMove[1].y][lastMove[1].x] = '@' end
realm[lastMove[1].y][lastMove[1].x] = '@' local xFinal, yFinal = lastMove[2].x + lastMove.x, lastMove[2].y + lastMove.y realm[lastMove[2].y][lastMove[2].x] = realm[yFinal][xFinal] realm[yFinal][xFinal] = nil end
Map
entity (this is much easier than trying to regress it) and issue an event with the revised move count to force the interface to update.realm[yFinal][xFinal] = nil self.World:Load(self.Map) self:dispatchEvent{name='Move'; count = #self.History} end
At this point, undo should work. You can load the app to your device and test it the real way, or it will simulate device shakes although the simulator doesn't supply most accelerometer
input.
3.137.162.110