Orkn's Pulp Prototypes

I love your solution for smoothing out the player movement, thanks for providing that demo!

How did you get the player to smoothly move behind tiles? I've been using hide for that, but when moving smoothly the player is drawn above the tile as they exit. It doesn't look great, especially when walking between two tiles hiding the player, since the player pops up briefly moving between the two. Any pointers would be appreciated if you have the time, thanks!

1 Like

Bumping this with some Resonant Tale progress!

resonant_tale_pot_boss

This is a sub-boss in the first dungeon. There are a few different mechanics going on:

  • When you step into the room the door slams shut behind you, trapping you in.
  • When you approach the key it disappears, triggering the sub-boss. I trigger both this and the door slam with an item tile that is identical to the floor in appearance but on collect calls an event to say it has been collected on the room. That makes the item tile reusable and keeps the room specific logic inside the room script.
  • I made quite a bit of use of both play ... then and wait ... then for timing events.
  • The boss requires the player to attack the correct (non-jiggling) pot. The others explode which damages the player - I have a generic event for an explosion happening at some coordinates that will trigger an "explode" event on the adjacent tiles as well as damaging the player and I re-use it for the player's bomb item they get later in the game.
  • Once the boss encounter starts it is on a timer to re-randomise the pot locations (I called each randomisation a "round" of the boss). I had to account for the round being ended by the player attacking the correct pot and make sure the timer didn't prematurely end a new round that had just been started manually by the player.
  • The pot locations are randomised! This is the big one. I wanted the pots to appear randomly anywhere in the room each round, but never in the same location as each other or the player. This isn't as simple as it sounds given Pulp's limitations!

All pulpscript has is a random function that returns a random integer between some min and max values (inclusive). After a bit of thinking what I realised is that, if you have a room where the possible x coordinates go between x_min and x_max, and the player is somewhere in the room, you can use the random function to randomly get an x coordinate that isn't the player's by getting a random number between x_min and x_max - 1, then comparing it to the player's x coordinate and adding 1 if it is equal to or greater than the player's x coordinate.

Like this:

min_x = 9
max_x = 15

tile_x = random 9,14
if tile_x>=event.px then
  tile_x++
end

Maybe this is obvious, but it was an "aha!" moment for me!

Unfortunately with three different pot locations to randomise it got a little more complicated. For each subsequent location I had to reduce the max in the random range (essentially you reduce it by however many values you already have occupied), but I also had to recursively check if my random value was equal to or greater than each of the already occupied coordinates. So the first pot has just the one conditional, the second pot has a top-level if/elseif with a nested if in each, and the third pot has a top level if/elseif/elseif each with a if/elseif in turn each with a nested if. I can just about accept that, but randomising the locations of more than 3 objects and some kind of refactor would be necessary - exactly how I don't know!

Anyway I hope some of that was interesting and I will endeavour to actually get the game finished!

3 Likes

First of all, this might be my favorite thread here. I love all your prototypes and also every bit I’ve seen from your ‘main’ game.

Regarding the scaling up of randomized tile placement, couldn’t you just check for the type of tile that is at the random coordinates.
I.e. if it’s a world tile (which means empty) place the pod, if it’s a sprite (there is already a pod at the coordinates) choose new randomized coordinates?

Thank you!

Yeah for sure, this would work. What I don't like about doing that is it has no upper bound on how long it might take - if you have quite a small/busy area so the chances of clashing locations are high, or just get unlucky, you could be looping several times each time getting a new random coordinate and then checking if it is free. This could - theoretically - go on forever! That's probably not a particularly big concern, in most situations including this one I think the chances are very good you will very quickly find a free location, but the uncertainty bothers me. What I like about my approach is that you are guaranteed to only have to call random once for each object.

Yes, please endeavor to do so!

Forget the above - why write elegant code when you can just brute force things!

resonant_tale_warp

I quite like how this warp effect looks. The code on the other hand...

on collect do
	ignore
	wait 0.25 then
		pdx = 0
		pdy = 1
		wait 0.25 then
			pdx = -1
			pdy = 0
			wait 0.25 then
				pdx = 0
				pdy = -1
				wait 0.25 then
					pdx = 1
					pdy = 0
					wait 0.25 then
						pdx = 0
						pdy = 1
						wait 0.2 then
							tell event.room to
								call "warp"
							end
							pdx = -1
							pdy = 0
							wait 0.25 then
								pdx = 0
								pdy = -1
								wait 0.25 then
									pdx = 1
									pdy = 0
									wait 0.25 then
										pdx = 0
										pdy = 1
										listen
									end
								end
							end
						end
					end
				end
			end
		end
	end
end

I think we should coin a term for this kind of common pulpscript pattern - I like "apocalyptic", because at the bottom of the event is the end of all things :playdate_cry_laugh:

6 Likes

A new game "camera pan" intro (excusing the placeholder title text):

resonant_tale_intro_scroll

This uses 3 rooms (title screen, full screen beach, beach with black HUD background) - the "pan" is done with config.follow and changing the center Y position, with the water tile as the overflow. Much simpler than using draw (which would probably have performance issues) or creating many rooms. I quite like the effect :playdate_happy:

(The pan in the gif looks slightly choppier because of how I captured it, but you get the idea)

What a great idea! I was wondering how do to something like this.

A couple of techier (for Pulp) bits I thought might be interesting to share:

Implementing simple stealth

I wanted to make some simple line-of-sight stealth mechanics - think being spotted by Pokemon trainers. I thought through a few different ways I might achieve it.

The naive approach would be to loop over each guard (or whoever is trying to spot the player) every frame and then look out along their line-of-sight until you find the player, hit a solid wall or reach the edge of the screen. With a bit of code this could work but it'd mean adding code to (or called from) the game's loop event and it could be quite impactful if I have several guards on screen and am needing to check each guard's line of sight every frame.

My first "trick" was to flip around the way of thinking about line-of-sight and instead start from the player. That means only worrying about four lines-of-sight in the cardinal directions from the player.

My second trick was to decide to use chained events to propagate along and check the lines-of-sight rather than having some code do it from the loop event. Every frame I already call an event on the tile at the player's location called "playerPresent" (I use it for hiding the player and taking damage on spike tiles). I realised I could add a "playerPresent" event handler to my floor tiles that would trigger four events, one for each cardinal direction, that would call the same directional event on the next tile in that direction. Provided the next tile in that direction similarly implements that directional event the event will propagate outwards along that line of sight. If it reaches a guard, the event will be called on the guard, and I can then check if the guard is facing in the right direction (from their current frame) and have them react accordingly. Even better, when laying out rooms I only need to use floor tiles implementing these events if they will potentially be in a guard's line of sight (so while I could have the events on every floor tile, it's cheaper not to). All the code ends up contained in the relevant tiles and there is zero overhead otherwise as I already had the playerPresent event.

That implementation looks a little like this (the "v" tiles are placeholder floor tiles implementing the directional events):

pulp_simple_stealth

One neat advantage of this approach is that doing something like pushing a block into the guard's line-of-sight will block their sight without me coding for that specifically!

Randomising on which tile something happens from a set of tiles

Or in other words - whack-a-mole!

pulp_resonant_tale_randomised_enemies

This mole enemy will popup from a random mole hole and throw a stone in the cardinal direction closest to the player.

I wanted to randomise which hole the mole would pop up from but without hardcoding the locations of all of the mole holes on the screen, and also without using loads of variables to store the number of holes and their locations.

After a bit of thinking, the solution suddenly occurred to me using only one variable. When I want to spawn a mole I first set this variable to be 0. I then emit a "spawnable" event. The mole hole tile implements this event to simply increment the variable, so when the emit completes I have a count of the spawnable tiles. I then reassign the variable using the random function to get a number between 1 and the variable itself i.e. the count of spawnable tiles. This is effectively the index of the list of spawnable tiles where I want to spawn the mole. I emit a second event, "spawn", again implemented by the mole holes. This event checks the variable - if it is greater than 1, it decrements it and finishes. If it is equal to 1, it spawns the mole and then decrements. If it is equal to 0 it does nothing. You can think of emit as looping through a list, and my randomised variable decides at which index in the list something should happen.

Now I can add, remove and move mole holes around a room and know that the spawning will always be randomised correctly, no further effort required!

2 Likes

Here's how that line-of-sight stealth is shaping up in-game:

resonant_tale_jail_stealth

The screen wipe transitions are implemented using crop. Unlike other approaches, crop doesn't have to be called from the player's draw event, so I could keep the code for the screen transitions all self-contained.

6 Likes

Hey, curious if I can use this demo code in a zelda-like project I'm currently working on. Would be sure to include your name in the credits and identify your portions of code in the comments of my own :playdate:

Sure, go ahead and use the movement code from the demo, I'm glad to be of help! Just please don't use the player tiles/character art itself :slight_smile:

It's been a while again since I've posted anything here, but I have been steadily working away on Resonant Tale. The big news is that I now have a collaborator for music and sound! My own efforts at them (even with Pulp's simple interface) proved woefully inadequate so I'm pretty excited. Good sound really is transformative! It's great being able to get an insight into that side of things by working with someone who knows their stuff :slight_smile:

On the subject of smooth movement, my approach here in some ways already feels outdated since people have found draw can accept non-integers, which can free you from the grid constraints entirely (with a bit of effort). I'm already tied in to my approach, which is fine as it works for what I want it to do, and the last thing I did was extend it so that the player can be smoothly animated while descending a ladder - like this:

resonant_tale_ladder

(While on a ladder you also can't turn to face left or right, and you can't place bombs or shoot arrows.)

2 Likes

Absolutely, thank you! And of course not- I'm doing all my own art :playdate:

1 Like

Over the last couple of days I revisited my tile-based pulp platformer prototype to shape it into a little "arcade cabinet" for inclusion in another soon-to-be-revealed project I was approached about.

My goal was to:

  • Make a proper level
  • Crop the screen to an arcade cabinet feeling aspect ratio
  • Implement coins to collect with a saved high score
  • Implement a timer with a saved high score
  • Give the game some polish with little things like a title screen
  • Refactor all of it to be self-contained and as easy as possible to drop into a larger pulp project!

Here is what it looks like right now:

tile_hill_pits_and_spikes

tile_hill_springs_and_saws

Implementing coins

The code is simple, but it's a lot of effort!

Coins are an item tile with this basic script:

on collect do
	
end

on pickup do
	thill_coins++
	tell event.room to
		call "pickupCoin{event.x}x{event.y}y"
	end
	swap "white"
end

collect is overriden to do nothing because of the non-standard player movement. Instead every frame I call the pickup event on the player's location.

The implementation of screen scroll (by having overlapping rooms with inset edge exits stitching them together) means that the "same" coin will be placed in multiple rooms. When the player picks up a coin in the level it not only needs to be swapped out of the current room, but any other room in which it is visible. For this reason every coin has its own variable to track whether it has been collected, and when picking up a coin the room script has a matching event to set the correct coin variable:

on pickupCoin2x10y do
	thill_coin_1 = 1
end

There is an event like this for every coin that can be picked up in that room.

When entering a room the coin variable needs to be checked to see whether each coin visible in the room should be swapped in or out. Swapping in as well as out means the level can easily be reset without restarting the entire game:

on enter do
	if thill_coin_1==0 then
		thill_tile = "thill coin"
	else
		thill_tile = "white"
	end
        tell 2,10 to
		swap thill_tile
	end
end

That's it for the concept - but multiply up for every coin (each appearing in multiple rooms) and it's a lot of code and a lot of copy/paste while changing the numbers!

Making it portable

I refactored extensively to try and make it as easy as possible to just drop in to another project:

  • "Portable" means "small". The game uses 11 rooms (10 for the level and 1 for the title screen). In addition to the default black/white world tiles the game only uses 10 player tiles, 11 sprite tiles, and 4 item tiles (cut down from the original prototype).

  • Every variable starts with thill_ so there shouldn't be any conflicts despite pulp having only global variables.

  • All of the rooms and tiles similarly start with thill so they will be grouped together in the pulp editor's alphabetically sorted lists

  • All of the code is self-contained within tile and room scripts, primarily a tile hill code tile (that doubles up as the coin HUD icon, just to save on tiles!). The player and game scripts have the minimum hooks to relay the required pulp events to the Tile Hill code. Essentially to start Tile Hill from within another pulp game someone just needs to call the one boot event to start the game and ensure those events are relayed for the duration (probably just with a flag that is set when the game is running).

  • Pressing "B" while playing will return the player to the title screen. Pressing "B" on the title screen will call a quit event which is intended to quit the player out of Tile Hill and back to the containing pulp game. This requires a little bit of tidying of config changes and resetting the screen crop.

Hopefully that's mildly interesting!

2 Likes

ART& was just released on catalog - check it out and head to the in game arcade if you want to give Tile Hill a play. It's very cool to have contributed something to a catalog release!

1 Like

I still think what you did with it is magic

Resonant Tale is finally approaching a complete game, and while it's not quite finished yet, yesterday I spent a surprisingly short amount of time trying to make the game more accessible. I thought my experience might be interesting to share :slightly_smiling_face:

Accessibility in Resonant Tale

Previously I have implemented some cheat codes in Resonant Tale that can be entered on the title screen, partly for fun and partly for making testing easier. One of those cheats was player invincibility, but it occured to me this would really be better as an easily discoverable option in some assistance settings. That prompted me to consider accessibility in the game in general.

I was a bit trepidatious about exploring accessibility. It's not something I'm familiar with or have consciously addressed before and I was worried about how much work the additional functionality would be, especially using Pulp. Obvious accessibility features that sprung to mind having seem them in other games, like adjustable font sizes, are not readily supported by Pulp (which I think is a fair trade off with Pulp's intended simplicity as a starting point into game development). Nethertheless I started reading up on Accessible Player Experiences and Game Accessibility Guidelines, both great resources. I geared my mindset to thinking about what can be easily done rather than getting hung up on what can't!

More details below, but this is the assist menu as it now looks in game:

resonant_tale_assist_menu

Settings persistence

Up until now my approach to save data has been really simple - if the player selects Continue from the title screen I use restore while if the player selects New Game I use toss. With assistance settings however I want them to persist even if a new game is started. I replaced my toss call with a custom game event (also called "toss") that looks like this:

on toss do
	toss
	// Re-store persistent vars
	store "assist_damage"
	store "assist_bosses"
	store "assist_crank"
	store "assist_timing"
	store
end

This works great and is going to be useful beyond these assist settings as I'm planning on keeping a record of whether the game has ever been beaten so I can offer a New Game Plus!

Player damage

I already had a version of this implemented in my invincibility cheat so this was easy. From an accessibility perspective being able to turn off player damage is a maybe heavy handed but simple solution to a host of potential issues.

I already had all player damage implemented through a player event takeDamage so this was as easy as adding this one conditional to the start of the event handler:

on takeDamage do
  if assist_damage==1 then
    done
  end
  // code handling player damage
end

Boss battles

I'd describe Resonant Tale as "low combat" - there are numerous hazards but few traditional enemies. Bosses are the exception as tests of player execution and as barriers to progress. While a valuable aspect of the game, making them optional prevents them blocking someone from enjoying the rest of the game which is not as challenging or demanding of execution.

Again I already had logic in place for triggering bosses, previously there to prevent bosses from being fought multiple times if returning to the boss room for example. This made implementing a simple skip almost trivial as (for the most part) I only needed to alter a few conditionals. The first dungeon's boss, for example, has a conditional changed from this

on enter do
  if has_bell_of_vitality==1 then
    // code toggling the boss
  end
end

to this

on enter do
  flags = has_bell_of_vitality
  flags += assist_bosses
  if flags>=1 then
    // code toggling the boss
  end
end

(The flags>=1 pattern is a pulpscript version of an or)

Crank puzzles

Input, especially analogue input, is an obvious consideration for accessibility. Resonant Tale has minimal use of the crank, used only in a couple of instances, only one of which is required to beat the game. A setting to remove those instances to prevent a small aspect of the game stopping someone from playing it entirely seems an obvious win!

With the assist option enabled, for an optional puzzle I simplify the puzzle to no longer require the crank. For the required puzzle simplifying it was not an option so I just made it pre-solved (although I may revisit this to make an alternative solution instead).

Timing puzzles

Timing puzzles refers to anything (outside of bosses) where precise timing is required from the player. This covers a relatively significant minority of Resonant Tale and was the most work to implement as I took different bespoke approaches depending on what content I was changing.

Take this room as an example - it has retracting spikes that the player must time passing over to not take damage:

resonant_tale_retracting_spikes_without_timing_assist

With the assist option enabled I replace the retracting spikes with static spikes and add a path through for the player to navigate:

resonant_tale_retracting_spikes_with_timing_assist

Not every timing puzzle has quite as nice a replacement as this and in multiple places I simply removed the challenge entirely, but I like being able to offer an alternative like this where I can!

2 Likes

Really excited that a reveal trailer for Resonant Tale just featured in the Playdate Community Direct! Here it is if you missed it :smile:

2 Likes

This looks wonderful! Amazing it's in Pulp too!
:trophy::crossed_swords: :shield:

1 Like