hawkthorne/hawkthorne-journey — pull request #1729 by campadrenalin (2013-09-16 02:28:58+00:00)
Title: Non-global controls objects

I said I'd do it, and I did it. This is a new InputController class that supports all your favorite current features, like saving and loading and getting the same instance that's already in memory, but also simplifies a lot of future work because of its approach toward presets and ownership.

# Usage

There are two basic uses we'll see in code:

## "Default" controller

Used wherever there is no sensible per-player controller context, like the main menu. In some ways, this is analogous to the old "only player 1 can do menu stuff" model, but that would be easy to change in the future.

controls = require('inputcontroller').get() -- Use ref to existing instance if available
...
if controls:isDown('START') then ...

## Player-owned controller

This is used throughout much of the "game" stuff, for example, the current player object is now passed into the pause menu, such that in a future multiplayer environment, only the player who paused could control the menu. In most cases, we make a local reference to the control object in functions where we use it, for efficiency, visual clarity, and minimal footprint in existing code.

self.player = player -- in some init function
...
local controls = self.player.controls
if controls:isDown('START') then ...

# Non-obvious changes

## button -> action

I started calling the START/JUMP/INTERACT things Actions instead of Buttons. This makes a lot more sense to me personally, and hopefully to other people as well, than trying to remember "which is virtual" out of buttons vs. keys. It seems like a bad thing to have to enforce and remember a difference between synonyms. That said, I haven't changed every "button" reference in the code to "action" in some global find-and-replace madness. The most important parts are changed, and if people like it, we can incrementally sweep the rest of the code.

## Requires

Some files lost unused requires of 'controls', some files that needed a player context were passed one, etc.

## Platform dropping

A rather dramatic change is platforms. They did their own listening to the down arrow, did their own timing tracking, and did not have any care about per-player tracking, which would be a bitch to fix with multiple Player objects standing on platforms (any player drops, all players drop). This is the sort of thing that the new control model forced me to address more immediately.

First of all, most of the state tracking has moved to the Player class, so that we can track a finer grain of drop relationship. We now have three different drop states that a player can be in:

1. Not dropping (self.platform_dropping == false)
2. Dropping from no place in particular (self.platform_dropping == true)
3. Dropping through a specific platform (self.platform_dropping == )

Platforms themselves do not track this sort of state at all anymore, they just read from the player's state.

The Player has a since_down variable tracking the time since the DOWN action was released. This allows it to detect during a DOWN keypress that we have a "double-click" of the DOWN action. When this happens, we set self.platform_dropping = true. We also set a timer for 0.25s, to set self.platform_dropping back to false, no matter what state we're in.

During collision with the player, a platform will first detect if player.platform_dropping == true. If so, player is dropping, but from nowhere in particular, and the current platform can "claim" it. The platform sets player.platform_dropping = self. After this check, it does a separate check if player.platform_dropping == self (which will be true until that timer goes off). If the player is indeed dropping through the current platform, specifically, we return out of the collision function early, to ignore it.

## Saving is no longer an implicit side-effect of making changes to a controller object

You have to do it explicitly with `InputController:Save()`. This is mostly to make it harder to shoot yourself in the foot if you're trying to mess with preset construction/customization, and don't want to accidentally overwrite your old preset data.

## Fairly nice test suite

It covers pretty much everything that's self-contained. Which isn't everything, for example, I wasn't sure how to test isDown. But the leftovers should be pretty obvious success/failure if you actually run the game.

In the future, we probably want to think through how to do really nice integration tests, which should be possible since we're already using Love itself in our test harness. I say this partly because I was able to rip the guts of controls.lua to shreds, and "make test" didn't complain until I renamed the file.