The playdate-platformer library

Whenever I learn a new game engine I like to make a platformer, because they're deceptively tricky and you really get to lean engine.
This time I decided to package it in case anyone else wanted to use it too.

9 Likes

Thanks for sharing!
Looks great, to create a player actor I would extend from basePlatformer or from defaultPlatformer and overwrite the defaults?

To create an enemy I would extend from machine ?
Can you describe a bit how I could create some basic enemy behaviour, let's say an enemy which has a walk, attack and die animation.

How can level creation be scaled?

Have you consider making an integration with LDtk:

See github:

I'm working on writing up a tutorial for this but just been a bit busy.
You can have a look through the code as it currently is Here - Github. This will be using a few of the other toyboxes including the LDtk importer.

Looks great, to create a player actor I would extend from basePlatformer or from defaultPlatformer and overwrite the defaults?

Most of the time, extend defaultPlatformer and overwrite the defaults, basePlatformer is there if you really want to have fine control over the characters behaviour. Example - Github

To create an enemy I would extend from machine ?

I'd say extend Trigger, the state machine is something actors have, usually saved to something like enemy.sm Here's an example of a simple enemy. This one doesn't use a state machine as I felt it was a bit overkill but you could add it if you wanted.
Here's the source for how the state machine is linked to the default platformer.

How can level creation be scaled?

This can work with the LDtk loader pretty easily, Here's an example of a loadLevel function that uses LDtk

Have you consider making an integration with LDtk:

I do plan to add an equivilent of the addWallSprites to work with the solid, allowing easier integration with LDtk but that would be it.

Note:
if you try to run this locally, it's missing a few toyboxes as I've reverted it back a few steps to write the tutorial.
You'll need to add these toyboxes before the final folder will run.

{
    "toyboxes": {
        "github.com/NicMagnier/PlaydateLDtkImporter": "main",
        "github.com/RobertCurry0216/pp-lib": "1",
        "github.com/RobertCurry0216/roomy-playdate": "1",
        "github.com/DidierMalenfant/Signal": "1",
        "github.com/RobertCurry0216/parti": "main",
        "github.com/RobertCurry0216/more_math": "main"
    },
    "installed": {
        "github.com/NicMagnier/PlaydateLDtkImporter": "main@a11dbbfd28acebd2d6903a808ff6475bea11c3c9",
        "github.com/RobertCurry0216/pp-lib": "1.0.2",
        "github.com/DidierMalenfant/Signal": "1.1.0",
        "github.com/RobertCurry0216/parti": "main@19579a27e6980e157852c0a1ace13adb6918cb5d",
        "github.com/RobertCurry0216/more_math": "main@667f8a0ba7df1c49740b11c5738d9e382d5fb31f",
        "github.com/RobertCurry0216/roomy-playdate": "1.0.0"
    }
}
1 Like

Have you made any guides yet? I'm currently trying to add a "charge" attack that activates when you're running and press B, and there's not a lot of guidance on extending the state machine. Currently my idea is:

  1. Create a custom RunState class that checks for pd.inputJustPressed (how can I extend the input object?) and calls self.actor.sm:charge()
  2. Add my custom runstate with self:addState("run", ...) in init
  3. Add my charge attack state with self:addState("charge", ChargeState(...))

Is that how I'm supposed to do it? I looked at overriding DefaultPlatformer:update() but that didn't seem the way to go

Sorry I've been super busy, Here is the example I'm working on writing up, this shows how to add a new state to the player.

To do the charge state I'd probably do something like this:

Sub-class the InputHandler:

class("MyInputHandler").extends(InputHandler)

function MyInputHandler:update(buttons, actor)
  MyInputHandler.super.update(self, buttons, actor)
  self.charge_pressed = pd.inputJustPressed(buttons.charge)
end

Override the input handler in then init, (I didn't say you could/should do this in the docs which is an oversight :grimacing: )

-- in player init
self.inputs = MyInputHandler()

self.buttons = {
    left=playdate.kButtonLeft,
    right=playdate.kButtonRight,
    jump=playdate.kButtonA,
    charge=playdate.kButtonB
}

Create a new ChargeState like you said, and add it to the state machine:

-- in player init
self.sm:addState('charge', ChargeState(self))
self.sm:addEvent({name='charge', from='run', to='charge'})

Sub-class the RunState and add it to the state machine:

class("MyRunState").extends(RunState)

function MyRunState:update(inupts)
  MyRunState.super.update(self, inputs)
  if inputs.charge_pressed then
    self.actor.sm:charge()
  end
end
-- in player init, this will override the existing run state
 self.sm:addState('run', MyRunState(self))

You could alternatively simply add the charge to the player update as the event can only be triggered while in the RunState, calling it otherwise won't have any effect.

1 Like

Thanks so much, I was close! Multiple inheritance isn’t a thing so I might have to exploit lua to make some of these crossover states :thinking:

I needed an easier way to combine states, so I did wrote a little wrapper function. I made a few transition states to get my player into the ChargeState and SitState:

  • ChargableState - checks for the input and changes state
  • SittableState - adds a little sit animation every so often.

I can add this to my player's init to add the new states:

self.sm:addState("idle", WrapState(IdleState, ChargableState, SittableState)(self, it, options.idle))
self.sm:addState("sit", WrapState(SitState, ChargableState)(self, it, options.sit))
self.sm:addState("run", WrapState(RunState, ChargableState)(self, it, options.run))
self.sm:addState("charge", ChargeState(self, it, options.charge))

You still need to update the events, otherwise the transitions won't work. The function is hacky but seems to do what I needed it to:

edit: I realized my old method was modifying the base state, so I rewrote it in a hacky lua way that uses a namespace and generates a new class:

local function wrapFunction(base, func, others)
    local _func = base[func]
    -- Replace the base class's function
    base[func] = function(self, sm, name, from, tonumber)
        local skip = false
        for index = 1, #others do -- So we can wrap multiple at once
            -- If a function returns false, skip the rest
            if others[index][func](self, sm, name, from, tonumber) == false then
                skip = true
                break
            end
        end
        if not skip then
            _func(self, sm, name, from, tonumber)
        end
    end
end

local wrappedStates = {}  -- This will be our "namespace" for the classes
function WrapState(base, ...)
    local wrappers = {...}
    -- Create the new class name
    local className = ""
    for index = 1, #wrappers do
        className = className .. wrappers[index].className:sub(1, -6)  -- Remove "State" from end
    end
    className = className .. base.className  -- Final result "RollableSittableIdleState" or "RollableRunState"

    -- Create our new class in our local namespace
    class(className, nil, wrappedStates).extends(base)
    local wrappedClass = wrappedStates[className]
    -- Wrap all our state functions
    wrapFunction(wrappedClass, "onenter", wrappers)
    wrapFunction(wrappedClass, "update", wrappers)
    wrapFunction(wrappedClass, "aftermove", wrappers)
    wrapFunction(wrappedClass, "onleave", wrappers)

    return wrappedClass
end

I've added some more examples and have finally put out the demo game I've been sitting on for a while

1 Like

Added a new FollowCamera class, comes with a few different modes such as look ahead, box, locked, etc.

And there's now a published game made with the pp-lib!

1 Like