Orkn's Pulp Prototypes

Something silly I needed to get out of my head as I could see how simple some of the mechanics would be to implement just with the default pulp collect behaviour...

Carrie Cargo: Parody Walking Action

What if Death Stranding was on the Playdate... and British?! Collect cargo, earn claps, and reconnect Britain to the DATA CANAL NETWORK.

carrie_cargo_delivery

Collect more cargo for a multiplier! Crank to balance!

carrie_cargo_londoners

Watch out for LONDONERS and reconnect Britain!

carrie_cargo_yodellers

These YODELLERS will chase you for your cargo!

carrie_cargo_bikers

Don't get run over by these BIKERS!

carrie_cargo_intro_1

As expected there is a thrilling plot...

carrie_cargo_intro_2

...full of shocking twists!

It's very unpolished (default font and there's no sound whatsoever!) but if you'd like to give it a play, here's a pdx!

Carrie-Cargo_-Parody-Walking-Action.pdx.zip (38.2 KB)

3 Likes

Just announced and released during the community direct, Goodnight Universe is a micro audio game to fall asleep with about the heat death of the universe.

I've had this idea in my head since before the Playdate, but the lack of front or backlight on the console really makes it make sense as it can be played in the dark with no screen light causing a disturbance.

I was also inspired by Pulp's noise channel to add an extra feature where the game's title screen continually generates low volume noise, meaning it can double up as a passive sleep aid. It was fun to work out how to poke the pulp music editor in the right way to do that!

The game's trailer was created in Pulp and can be watched in-game by entering the Konami code on the title screen. I really can't resist including cheat codes in my games!

This is definitely on the more experimental and conceptual end of the scale but I think Playdate really encourages that kind of thing.

Goodnight Universe is free or pay what you want on Itch :slight_smile:

1 Like

Runaround Reaper was my entry for PlayJam 4 - my first ever game jam! I finished mid table which I was happy with, especially considering "use of console" was one of the three categories in which my game came deservedly second last (not using any unique features of the Playdate at all really).

Mainly though it was an excuse to have a go at refining some chaser enemy behaviour in Pulp. It's probably too intensive to do any actual pathing in Pulpscript, but what I ended up with was an enemy that can chase the player (or move towards another target) and navigate around rectangular obstacles. As long as there are no inside corners to get trapped in, it works :smile:

I wasn't planning on participating as I didn't think I'd have the time, but it was fun to take part in a jam, even if it was on my own and without the full weekend to build out an idea over. I'd definitely like to properly dedicate a weekend to a jam with a team if I get the chance in the future!

It's been a while since I posted anything here, but I've been playing with lots of ideas! Here are some of them:

Tourist

A pathfinding puzzle game. Visit every unvisted tile without going back on yourself.

tourist_teaser

This started as me wanting to make a game that uses only black and white tiles, which is an effective resolution of 25x15. This kind of simple puzzle might be familiar as it appears in games like Zelda and Pokemon, and it can often be found as a standalone game using a "lawn mowing" premise.

The dpad is used to solve the puzzles, while the B button is used to reset the current puzzle. The puzzles are arranged vertically so that the crank can be used to scroll through them. I also aimed for consistency in having the puzzles start in the bottom-left, so the player knows (roughly) where to look to begin. All of this was to solve for not wanting any other tiles, including text.

At first I just tried making abstract puzzles. They were kind of fun, but lacked charm. Switching up to picross-style images-as-puzzles felt a lot better! The releasable game I'm aiming for has several sets of puzzles, each around a theme and with their own linear progression. The clip above shows the start of a Resonant Tale set!

Splitting the game into sets does mean I now have a horizontally scrolling menu which does use text and tiles other than black/white in appearance, but I think that's a fair concession to making a more polished experience.

Little Hungers

An uneasy walk through the woods.

little_hungers_bowels

The forest at night is a dangerous place for a mouse.

little_hungers_perspective

I've only made a few screens, but I envision Little Hungers as a micro adventure game or walking sim. It's horror-adjacent, but more about general vibes of unease than actual frights or peril. I want every screen to be unique and visually appealing with differing perspectives yet minimalistic in use of tiles.

little_hungers_scrolling_forest

This is the last room I created, faking a scrolling forest that is larger than the screen. It's all fakery, the trees arranged to make a nice repeating pattern and with the player actually looping around the centre of the room, with the player's "coordinates" tracked in code (shown here for debugging) and the animals drawn in based on those coordinates. I think it's pretty effective!

Bunk-A-Bust

A vertical orientation crank controlled bomb survival game.

bunk-a-bust-teaser

Back when I was playing Little Big Planet on PS3, "bomb survival" levels were all the rage. I'm not sure where the specific genre originated, but in LBP they were quick and easy to make and fun to play, so there were loads of them. I thought a crank controlled Pulp-made variation would be a fun project, but I lost motivation given real-world events. It's parked for now, but I would like to revist it as the core gameplay is fun!

Pulp Fishing Prototype

A proof of concept for top-down fishing with the crank!

pulp_fishing_prototype

Probably the most complex code-wise of all of the above, this proof of concept is fully playable. It's hard to tell without UI elements and actual visuals, but there are multiple stages to fishing. You can move around to find a spot, then get locked in place when you cast your line. Casting involves you selecting distance and direction which are based on rod stats. You then have to wait for a bite and press A quickly enough to hook the fish, then reel it in with the crank while it fights against you - there's line strength in there too, just without any indication yet! Similarly unclear is that the fish you hook have their own stats (like exhaustion) and each water tile has its own set of fish that are catchable, with odds that can vary by tile and can be modified by your choice of lure.


That's all I'd like to share for now... but I've also been thinking about possible sequel ideas for Resonant Tale, as well as wanting to revisit some of my earlier pulp prototypes! I should definitely pick something to stick with through to completion but... that's always the hard part!

I'd love to hear what (if any) of the above people think is most exciting :slight_smile:

4 Likes

Thanks for sharing these, it's nice to have a few projects going on to keep the ideas flowing!

I actually really like the look/idea of Little Hungers, partly as I think the basicness of pulp tiles can encourage more imagination on the part of the player, hooking into that sense of unease. I also like that you've got different perspectives in there, and maybe there's scope to play with expectations in the style of something like Superliminal :upside_down_face:

Maybe you could even get the portrait mode view in there too, to really disorient players? :slight_smile:

1 Like

Just read through this whole thread - amazing work. Grabbed Resonant Tale to play through with the kids. They were enthralled by my 7DRL entry from this year, think this is truly gonna blow their minds.

1 Like

Thank you so much, hope your kids enjoy it!

New post, new project! Lolife is an old school action RPG.

lolife-entities

It's very early days but there's quite a lot going on here already!

Multiple instances of a single sprite

The mice in the gif above are all instances of the same sprite. That might not sound like a big deal, but if you've tried to do similar in Pulp you might know it's surprisingly difficult!

lolife-sprites lolife-mouse-frames

As you can see, there's only one mouse sprite with four frames, one for each direction the mouse can face.

There are a handful of challenges to making this work in Pulp:

  • All variables are global
  • Variables can't be dynamically referenced (at least not directly)
  • Sprites are tiles like any other - swapping them into a room will replace the tile at that location

Global variables make it difficult to separately track health for multiple instances of an enemy. They also make it difficult to keep track of the tile the enemy is "above", something you need to do given there's no record of the tiles swapped out of a room.

One (very reasonable!) solution is to duplicate a sprite and edit the variable names. That way you can easily place multiple enemies in a room as they are actually different sprites (even if they look and behave the same), but it's a bit messy and annoying to have so much duplication of tiles and code. I wanted to do better and find a way of avoiding this duplication!

My solution involves "registering" each enemy on entering a room. Take this "river" room as an example, which is empty of enemies in the editor:

The room script looks like this:

on enter do
	entity_1 = "mouse"
	entity_1_x = 8
	entity_1_y = 7
	
	entity_2 = "mouse"
	entity_2_x = 15
	entity_2_y = 3
	
	tell event.game to
		call "registerEntities"
	end
end

The game script handles tracking each enemy (I call them entities as they could just as easily be friendly NPCs) in a generic fashion. There's a bit of code duplication between each numbered entity, but it's a lot less (up to the number of entities that can be on screen at the same time) than having to duplicate every sprite to the same number.

The mouse sprite itself has a similarly simple-looking script:

on getMaxHP do
	max_hp = 3
end

on tick do
	tell "behaviour" to
		if aggro>0 then
			call "checkAdjacent"
			range = 3
			call "checkRange"
			if player_in_range==1 then
				call "flee"
			else
				call "randomWalk"
			end
		else
			call "checkAdjacent"
			call "randomWalk"
		end
	end
end

on attack do
	aggro = 1
	xy = "{event.x}x{event.y}y"
	tell event.game to
		call "damageEntity"
	end
end

The trick here is that the "behaviour" sprite contains all of the complex code to do with movement, but it's generic - checkAdjacent for example works out if the four adjacent tiles are able to be moved onto, assuming x and y variables have been set as the coordinates of the sprite. By separating out this complex but generic code, each enemy can have its unique behaviour easily constructed by stringing together the desired generic behaviours. As you can see here, a mouse will walk randomly or, if aggro'd (which happens on attacking any mouse on the same screen), will flee from the player if the player gets too close. You can see this behaviour in the gif.

The tick event is called from the game loop on each registered entity, and the game script then handles the actual swapping of tiles etc. based on variables changed by the sprite's script.

I've not shown that more complex code for handling entities or for behaviour, but the point is that now it is written I don't have to worry about it. If I want to create a new enemy, I just create one sprite and add a tick event with simple, strung together behaviour calls. If I want to create a new room with 3 enemies in it, I just register the three enemies in the room's enter event by name and starting coordinates. All the hard work is already done!

5 Likes

Lolife now has enemies that can attack the player, healing, experience and levelling.

lolife-combat-healing-levelling

While the stats being tracked are relatively simple, balancing is anything but! I want each level to feel like a significant power up, so rather than increasing the player's health and damage linearly (which would feel increasingly less meaningful) I decided to go with a percentage increase. At the moment that's settled on +30% damage and +10% health - the lower health increase keeps enemies threatening even as your ability to kill them steps up.

I ended up making a spreadsheet to lay out stats by level for the player and different enemies so that I could see how the numbers change and find a good curve. While I just have health and damage as the base stats, from those I derive hits-to-kill and hits-to-die (at each level, how many hits the player will kill an enemy in, and how many hits from an enemy the player will die in). The ratio of those multiplied by some constant is used to derive the experience the player will gain on killing an enemy, the result being that more dangerous enemies yield more experience. I also have it so that once you can kill an enemy in a single hit, they no longer yield any experience. That prevents someone from grinding against the same weak enemies indefinitely, but there's still a use to those enemies thanks to how healing works.

The player heals whenever they kill an enemy by an amount related to the enemy's damage. In similar games like Hydlide and Ys the player can heal by standing still, but I didn't want something so inactive. By tying healing to killing it encourages more aggression and adds some potential strategy when fighting stronger enemies where weaker enemies are also present and can be used to heal mid-combat. It's similar to God of War or Metal Gear Rising!

I need to do some general performance optimising, but with these core mechanics in place I can now start to think about making content rather than mechanics, which I'm excited about as I generally prefer creating game content!

6 Likes

I need to do some general performance optimising

Here's a small but interesting one with the HUD!

Before, my event for drawing the HUD (called from the player's draw event) looked like this:

on drawHud do
	x = 18.5
	label "Lolife" at x,1
	label "Health" at x,10
	label "Target" at x,12
	label "Lvl {2, :player_level}" at x,7
	label "<===>" at 19,8
	fill "white" at 153,66,level_px,4
	label "<===>" at 19,11
	fill "white" at 153,90,health_px,4
	label "<===>" at 19,13
	fill "white" at 153,106,target_px,4
end

Performance wise these are all quite expensive functions. There aren't too many calls here, but as I'm not hitting a solid 20fps with multiple entities on screen every little helps.

As a side note, the <===> labels (which are for the border boxes of the xp/health/target bars) are already a little optimised in that using font characters is cheaper than embedding tiles.

Generally though I could see there were improvements to be made.

The obvious alternative is to bake the fixed HUD elements into each room. The problem there is that changing the HUD then becomes annoying as I'd have to manually go through each room to change the baked-in appearance. I'd also have to create tiles for the HUD text as font tiles can't be placed into rooms, which is a bit messy.

Luckily a third option occurred to me, and it takes advantage of that x = 18.5 line at the top and some exploits of the pulp engine!

Officially you should only be able to place labels or draw tiles at integer coordinates. If you try to call e.g. label "test" at 10.5,12.5 or similar, it will get flagged as invalid pulpscript. You can however get around this by assigning those non-integer values to variables and then passing those variables to the function. This lets you draw tiles and labels "off-grid", but with the downside that pulp's internal handling of partial screen refreshes - something you don't normally have to know or worry about - breaks when you do this.

While normally a label will only be displayed for the frame you actually call the label function in, an off-grid label won't get cleaned up until something else triggers that area of the screen to be refreshed. In other situations this is a problem that needs to be accounted for, but in this case I can make use of it! I realised I only actually need to call those labels in the first frame of entering a room. Being off-grid they won't get refreshed by subsequent frames as nothing else is causing a refresh in that area of the screen.

With that in mind, my event now looks like this:

on drawHud do
	if draw_offgrid_labels==1 then
		x = 18.5
		label "Lolife" at x,1
		label "Health" at x,10
		label "Target" at x,12
		label "Lvl {2, :player_level}" at x,7
		draw_offgrid_labels = 0
	end
	label "<===>" at 19,8
	fill "white" at 153,66,level_px,4
	label "<===>" at 19,11
	fill "white" at 153,90,health_px,4
	label "<===>" at 19,13
	fill "white" at 153,106,target_px,4
end

That's four less labels per frame, while keeping all of the advantages of not having any HUD baked in!

4 Likes

Nothing new to show (it looks and plays exactly the same) but I think I've reached the end of my current performance optimising of Lolife.

Before any optimisation I was losing about one frame per second for every entity on screen:

0 entities = 20 fps
1 entities = 19 fps
2 entities = 18 fps
3 entities = 17 fps
4 entities = 16 fps
5 entities = 15 fps

After optimising it's still not perfect, but it's a lot better:

0 entities = 20 fps
1 entities = 20 fps
2 entities = 20 fps
3 entities = 19 fps
4 entities = 19 fps
5 entities = 18 fps

How I got there was with a whole mix of changes.

Minimising use of draw and label

In addition to the "fixing" of off-grid labels in my post above, I baked in the health/target/experience bounding boxes to my rooms, bringing my labels per frame down to zero and leaving me with just three fill calls per frame.

Minimising swap and frame

In my first post on Lolife I shared how each enemy was a single sprite with multiple frames for each direction they can face. This was very neat and tidy but meant I was calling both swap and frame each time the enemy moved. By splitting each direction into a separate sprite I only need the swap.

This also solved a bug I found in enemy attacks and simplified that code as now I just use play for the enemy attack animation.

Minimising event calls

I managed to rewrite my way to an extra frame per second just by changing my "pick a random available direction" code from calling several events to just calling one. I'd suspected as much from when I had to change my line-of-sight code in Resonant Tale, but this confirmed it - calling events is not performant. I don't know why, but splitting code between events is not a good idea for performance (which is disappointing, because it's much neater to do that!).

Once I'd confirmed it was worthwile I rewrote a lot of my code to try and minimise the number of events I was using. Along with optimising the HUD this had the biggest impact!

Minimising scripts involved

In addition to event calls being slow, it seems like just telling another script to call an event is slower than calling that event in the same script. Again this is a bit annoying from a keeping tidy code perspective, but I needed all the savings I could get so I went ahead and moved most of my events and code into the game script. I went from events in the game script calling an event on the enemy sprite script calling events on a generic behaviour sprite script, to just calling events in the game script.

The enemy sprites now don't have scripts at all - which is actually quite neat now that I have split enemy directions into separate tiles.

Minimising variable assignment

This probably had the least impact, but I managed to optimise some of my code by being smarter about what variables I was using and the operations involved.


Overall I'm happy with the peformance now. A drop to 18fps is a lot less noticeable than a drop to 15 (and actually not really noticeable at all with pulp's tile based movement).

Received wisdom in software development is that premature optimisation is the root of all evil. While I haven't yet made much of the game, this optimisation wasn't premature - knowing the limits of the number of entities I can have on screen will inform my game design!

3 Likes

I think I've reached the end of my current performance optimising of Lolife

I thought wrong :smile:

Here's where I'm at now with entities on screen. The second column is with the enemies alerted, which is a little more complex in behaviour (checking distance to the player and chasing or fleeing, rather than just a random walk).

0 entities = 20 fps | 20 fps
1 entities = 20 fps | 20 fps
2 entities = 20 fps | 20 fps
3 entities = 20 fps | 19 fps
4 entities = 19 fps | 19 fps
5 entities = 19 fps | 18 fps

This was with some more refactoring of my game script, the behaviour is still unchanged.

One significant improvement in the random walk code was in replacing 126 lines of code... with 868 lines of code. The reason that sped things up (despite being a lot more verbose and repetitive) is that I reduced average number of calls of the solid function. Previously I was always checking all four surrounding tiles per entity, now that's the worst case scenario. Unfortunately that mean replacing some relatively tidy code with 24 permutations of very repetitive code. I ended up writing a python script to generate that pulpscript rather than copy-pasting and writing it all myself!

Another significant improvement was in further reducing the number of events being called from the game loop - it's now just one per entity when they are just moving around. The way I achieved that is by changing how each enemy's behaviour is defined. Before I had an event per enemy that strung together different events. I've managed to keep the ease of adding new enemies (and still instanced without sprite duplication) by switching to an approach where I define several attributes of the enemy and these translate into branching code in a single event. It's slightly less flexible but I think it's actually a better fit!

This is all the unique code needed to define my current two enemies:

on mouse do
	damage = 4
	hp = 12
	range = 3
	vigilant = 0
	aggressive = 0
	timid = 1
	nomadic = 1
end

on fox do
	damage = 4
	hp = 32
	range = 3
	vigilant = 0
	aggressive = 1
	timid = 0
	nomadic = 1
end

These stats get queried when each entity instance is registered on room enter so these events are only being called once on enter, not every time the entity moves.

This optimisation has been more than worthwhile, as not only have I significantly improved performance which lets me design with more entities per room, I've made adding new entities even easier. All I have to do is make one set of sprites (for the different directions and attack animations) and define one event like the above, then I can register any number of each entity in each room (up to my simultaneous entity limit).

I really will make some actual content and have some new gifs to share next time! I've already been designing the map and the overal flow/progression, so hopefully I'll have something to show soon :slight_smile:

3 Likes

And here it is, some actual content! This bear will chase the player if they get too close, and return to their spot when the player runs away.

lolife-bear-death

Also pictured: some new rooms, a bit more combat with healing and earning xp, and the player dying and respawning at a fresh grave having lost their progress towards their next level.

The bear's "return to spot" behaviour is the same code as for chasing the player, just with the target coordinates changed. Simple but effective!

4 Likes

Incredible work! Love the micro-optimizations in here - will definitely try out the sub-tile drawing hack myself.

1 Like

Thank you! A fun thing about Pulp is how micro-optimisations can actually matter :smile:

Here's another Lolife gif showing off a bunch of new content:

lolife-village-dialogue

  • "A" attacks as before, but "B" lets you interact (using act and config.autoAct = 0)
  • The dialogue box switches between the top and the bottom of the screen to avoid overlapping the player and whatever they are interacting with (wrapped up nicely in a reusable event - no need to worry about performance micro-optimisations as dialogue pauses the game loop!)
  • The guard only becomes hostile once attacked. Like the bear they return to their spot once the player gets out of range.
  • A new bull enemy is always hostile. Get out of range and they'll stop chasing, but remain where they ended up. This makes it easier to lead them away and sneak around!
  • There's an inventory! The HUD is now looking pretty full and complete.
  • The lantern lets you see in dark places. I spent quite a while iterating on its appearance so it looks good and recognisably the same both in-game and in the HUD.

That's not everything, but I don't want to spoil all of the content!

3 Likes

I've now got a save system in place, with a placeholder title screen on launch that lets you pick between "New Game" and "Continue". Saving in Pulp can be fiddly so I wanted to get this sorted and working now while it's still manageable!

The minimum save data looks like this:

{"player_level":1,"player_xp":0,"player_max_hp":10,"player_hp":10,"player_damage":5,"room":"cemetery","px":9,"py":7,"pdx":0,"pdy":1}

On selecting "Continue" from the title screen I call restore and then an event called init which does some other variable initialisation with the above. The init event also gets called when selecting "New Game", but instead of restore I call toss and then initialise those same stored variables to the new game specific values.

Next up I have some accessibility ideas I want to tackle. Like saving, I think implementing these now will be easier than much later into development, as it'll be easier to add saving and accessibility to new content as I make it rather than retroactively all at once.

1 Like

A little bit (or a lot) of save system refactoring later and now I have multi save slot support!

Here's a quick gif showing two different saves being loaded and then starting a new game. The title screen and menus are just placeholders so I could get something working:

lolife-multi-save-slots

This was something I wish I'd done in Resonant Tale, but it would have been a big change to make late on so I declared it out of scope. It's definitely more complex, but the premise is pretty simple - it basically just requires some methodical shuffling around of lots of variables!

The crux of my approach is to have a slot variable for the currently active save slot and to call events to save and load specific to each slot.

Saving now looks (pretty much) something like this:

on save do
	call "saveS{slot}"
end

on saveS1 do
	s1_player_level = player_level
	// etc.
	store "s1_player_level"
	// etc.
	store
end

Loading is similarly like this:

on loadSlot do
	call "loadS{slot}"
end

on loadS1 do
	player_level = s1_player_level
	// etc.
end

While deleting a save (when starting a new game in that slot) looks like this:

on deleteS1 do
	toss "s1_player_level"
	// etc.
end

Once you factor in all of the variables in the save data and the multiple save slots, that's a fair amount of repetitive and duplicated code, but it should be easy to manage going forwards.

1 Like

Lolife now supports real time and turn based modes!

lolife-real-time-turn-based-comparison

In real time mode (the default choice) enemies automatically act every 0.5 seconds (10 frames).

In turn based mode enemies alternate turns with the player - during one turn the player can move twice, move and attack, or just attack (i.e. attacking always ends their turn).

It's currently configurable via a new options menu on the placeholder title screen:

lolife-options-real-time-turn-based

I see this as an accessibility option, but also just as a preference for how people want to play - both modes should be balanced and legitimate ways to experience the game. The balance is different between the modes, as in real time mode the player can quickly land multiple hits on an enemy between enemy attacks, but I think the easier tactical approach of turn based cancels that out pretty nicely. You might end up grinding a little more in turn based mode because of it, but it's easier to do so!

This was surprisingly simple to get working thanks to the way the game loop was already set up. Basically I have a tick variable that gets decremented each frame until it reaches zero (or less), then the enemies act and tick is reset (to 10). Now I check a new mode variable and only decrement tick each frame if in real time mode. In turn based mode I instead decrement tick in the player's update event (by 5) when moving or in the player's confirm event (by 10) when attacking. There are a couple of added wait/ignores so the enemy turn doesn't happen immediately on player input, but that's pretty much it!

6 Likes

The placeholder title menu has been replaced!

lolife-menu-options

It may well still change, but we've got a logo now! A couple of new assist options have also been implemented in addition to the choice between real time and turn based gameplay.

The menus here are all made using label, window and draw. The player is hidden and trapped between solid world tiles so that the bump event can be used to drive the cursor logic. This is more complex than using the built in menu function but it's also a lot more flexibIe. I think it's worthwhile!

(Bonus update for the eagle eyed reader - you might notice a new enemy in that gif! The fox was too visually similar to the mouse, this boar is a lot more distinct.)

3 Likes

Hey Orkn I was using that json smooth movement demo to put my own character instead of yours to see what it would look like, and when I finished switching the sprites out for my ones
when I went to test it it looked great but when the player went left the player would just freeze then not move.

Do you know how to fix this?

[Video 1 when I move left I cant move any more] Playdate pulp smooth movement not working 1 | Loom

[Video 2 showing all the sprites hopefully this video will help]
Playdate pulp smooth movement not working 2 | Loom](Playdate pulp smooth movement not working 2 | Loom

Hopefully I or someone solves this because its very unclear how or why this happens, well at least to me its unclear.