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.
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"
}
}
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:
- Create a custom
RunState
class that checks forpd.inputJustPressed
(how can I extend theinput
object?) and callsself.actor.sm:charge()
- Add my custom runstate with
self:addState("run", ...)
in init - 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 )
-- 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.
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
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 stateSittableState
- 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
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!