There was some interest in this topic on the Pulp discord channel, so I'm posting my tips and tricks here. Not saying it's the best way, just sharing what I've arrived at. All key commands refer to Mac keyboard layout.
What's this all about?
I'm making a tile-based puzzle game [EDIT: now released!], and I wanted to implement an undo feature so that the player can revert a move and go back one "step" in time. You can see a gif of this in action here .
Pulpscript is very simple, so for more complex or "dynamic" behavior like this, you have to write it out yourself. Explicitly defining every variable and event handler that could be used is tedious to do by hand.
For the undo feature, we need to keep track of the game's state at each step in time, which means we need to save many tile states at each step in time, which means we need lots and lots of variables. How many? For full room states, it's roughly 25 (tiles wide) * 15 (tiles high) * the number of states to save (the max number of times the player can undo), plus a bunch of special handling code for the player!
Even for a smaller example like a 3x3 Tic-tac-toe board, that's a lot of variables to manipulate. With just a few undos, some of the needed Pulpscript might look something like this:
Big chunk of Pulpscript
// ...
on setStateT0ToCurrent do
// log "@setStateT0ToCurrent"
stateT0_player_x = playerStateLatest_x
stateT0_player_y = playerStateLatest_y
stateT0_x_0_y_0 = name 0,0
stateT0_x_0_y_1 = name 0,1
stateT0_x_0_y_2 = name 0,2
stateT0_x_1_y_0 = name 1,0
stateT0_x_1_y_1 = name 1,1
stateT0_x_1_y_2 = name 1,2
stateT0_x_2_y_0 = name 2,0
stateT0_x_2_y_1 = name 2,1
stateT0_x_2_y_2 = name 2,2
end
on setStateT1ToCurrent do
// log "@setStateT1ToCurrent"
stateT1_player_x = playerStateLatest_x
stateT1_player_y = playerStateLatest_y
stateT1_x_0_y_0 = name 0,0
stateT1_x_0_y_1 = name 0,1
stateT1_x_0_y_2 = name 0,2
stateT1_x_1_y_0 = name 1,0
stateT1_x_1_y_1 = name 1,1
stateT1_x_1_y_2 = name 1,2
stateT1_x_2_y_0 = name 2,0
stateT1_x_2_y_1 = name 2,1
stateT1_x_2_y_2 = name 2,2
end
on setStateT2ToCurrent do
// log "@setStateT2ToCurrent"
stateT2_player_x = playerStateLatest_x
stateT2_player_y = playerStateLatest_y
stateT2_x_0_y_0 = name 0,0
stateT2_x_0_y_1 = name 0,1
stateT2_x_0_y_2 = name 0,2
stateT2_x_1_y_0 = name 1,0
stateT2_x_1_y_1 = name 1,1
stateT2_x_1_y_2 = name 1,2
stateT2_x_2_y_0 = name 2,0
stateT2_x_2_y_1 = name 2,1
stateT2_x_2_y_2 = name 2,2
end
on setStateT3ToCurrent do
// log "@setStateT3ToCurrent"
stateT3_player_x = playerStateLatest_x
stateT3_player_y = playerStateLatest_y
stateT3_x_0_y_0 = name 0,0
stateT3_x_0_y_1 = name 0,1
stateT3_x_0_y_2 = name 0,2
stateT3_x_1_y_0 = name 1,0
stateT3_x_1_y_1 = name 1,1
stateT3_x_1_y_2 = name 1,2
stateT3_x_2_y_0 = name 2,0
stateT3_x_2_y_1 = name 2,1
stateT3_x_2_y_2 = name 2,2
end
on setCurrentToT0 do
// log "@setCurrentToT0"
goto stateT0_player_x,stateT0_player_y
playerStateLatest_x = stateT0_player_x
playerStateLatest_y = stateT0_player_y
tell 0,0 to
swap stateT0_x_0_y_0
end
tell 0,1 to
swap stateT0_x_0_y_1
end
tell 0,2 to
swap stateT0_x_0_y_2
end
tell 1,0 to
swap stateT0_x_1_y_0
end
tell 1,1 to
swap stateT0_x_1_y_1
end
tell 1,2 to
swap stateT0_x_1_y_2
end
tell 2,0 to
swap stateT0_x_2_y_0
end
tell 2,1 to
swap stateT0_x_2_y_1
end
tell 2,2 to
swap stateT0_x_2_y_2
end
end
on setCurrentToT1 do
// log "@setCurrentToT1"
goto stateT1_player_x,stateT1_player_y
playerStateLatest_x = stateT1_player_x
playerStateLatest_y = stateT1_player_y
tell 0,0 to
swap stateT1_x_0_y_0
end
tell 0,1 to
swap stateT1_x_0_y_1
end
tell 0,2 to
swap stateT1_x_0_y_2
end
tell 1,0 to
swap stateT1_x_1_y_0
end
tell 1,1 to
swap stateT1_x_1_y_1
end
tell 1,2 to
swap stateT1_x_1_y_2
end
tell 2,0 to
swap stateT1_x_2_y_0
end
tell 2,1 to
swap stateT1_x_2_y_1
end
tell 2,2 to
swap stateT1_x_2_y_2
end
end
on setCurrentToT2 do
// log "@setCurrentToT2"
goto stateT2_player_x,stateT2_player_y
playerStateLatest_x = stateT2_player_x
playerStateLatest_y = stateT2_player_y
tell 0,0 to
swap stateT2_x_0_y_0
end
tell 0,1 to
swap stateT2_x_0_y_1
end
tell 0,2 to
swap stateT2_x_0_y_2
end
tell 1,0 to
swap stateT2_x_1_y_0
end
tell 1,1 to
swap stateT2_x_1_y_1
end
tell 1,2 to
swap stateT2_x_1_y_2
end
tell 2,0 to
swap stateT2_x_2_y_0
end
tell 2,1 to
swap stateT2_x_2_y_1
end
tell 2,2 to
swap stateT2_x_2_y_2
end
end
on setCurrentToT3 do
// log "@setCurrentToT3"
goto stateT3_player_x,stateT3_player_y
playerStateLatest_x = stateT3_player_x
playerStateLatest_y = stateT3_player_y
tell 0,0 to
swap stateT3_x_0_y_0
end
tell 0,1 to
swap stateT3_x_0_y_1
end
tell 0,2 to
swap stateT3_x_0_y_2
end
tell 1,0 to
swap stateT3_x_1_y_0
end
tell 1,1 to
swap stateT3_x_1_y_1
end
tell 1,2 to
swap stateT3_x_1_y_2
end
tell 2,0 to
swap stateT3_x_2_y_0
end
tell 2,1 to
swap stateT3_x_2_y_1
end
tell 2,2 to
swap stateT3_x_2_y_2
end
end
Here are a couple ways to make that scripting less tedious.
Tip 1: Multi-cursor text manipulations
This one is generally applicable to text editing, not just Pulpscript. Many text editors let you place multiple cursors down at once, so you can edit multiple lines at once.
You can try this out in the Pulpscript editor by holding down the Command key (โ) and clicking on multiple places to place cursors. You can also hold the Option key and press the Up and Down arrows to add cursors above and below your current cursor. The Pulpscript editor uses Ace which lists shortcuts here. That link is currently broken, but you can find other references online (like here and in the official source).
You can also copy and paste your code into your editor of choice. Just to give a few more examples:
- Here's what multi-cursor editing looks like in Panic's Nova editor: link
- In Sublime Text, pressing Command+D (โD) on a word will highlight that word, and subsequent presses will additionally select the next occurrences of that word, giving you cursors at all of them.
- In VS Code if your cursor is on a word, you can press Command+Shift+L (โงโL) and it will select all occurrences of that word in the file, giving you a cursor at each location instantly.
Learn the keyboard shortcuts for these operations, since you'll use them often once you get the hang of this.
Once you have multiple cursors placed, you can move them around as normal. This is an easy way to duplicate some big chunk of code, then make some slight change to the multiple copied lines.
Smooth moves
You'll soon find that sometimes you need to navigate around "words" (groups of certain characters next to each other) of different lengths.
// we have:
hoge-1 = ab
//...
hoge-21 = cdefg
// we want:
// hoge-1-x = ab
//...
// hoge-21-x = cdefg
Imagine we want to add "-x" after the numbers in two variable names "hoge-1" and "hoge-21" we have on two separate lines, so that they become "hoge-1-x" and "hoge-21-x", and we have two cursors, both directly to the left of the number in each variable name. In this case, hold Option before pressing right and typing the "-x". This moves each cursor by word, instead of by character, so that we don't end up changing these names to "hoge-1-x" and "hoge-2-x1" by mistake. This generally works for any text input in any program, though the characters used for word-separating can differ (like hyphens, underscores, other special characters). Also, holding Command before pressing left or right will move all cursors to the beginning or end of their lines.
You can also paste multiple different things into the places your cursors are highlighting, as long as the number of things to paste matches the number of cursors. This might also depend on the editor used.
In the below example, selecting the 3 numbers with 3 cursors, cutting or copying, then selecting the 3 'X's with 3 cursors and pasting should result in the numbers getting cleanly inserted into the corresponding "slots".
// before
2
4
6
hoge-X
hoge-X
hoge-X
// after
hoge-2
hoge-4
hoge-6
A word of caution
Bulk copy-pasting between sources does introduce the risk of accidentally overwriting your latest changes in one window with old changes from another window, so be careful! Also, I noticed that as a result of this copy-paste workflow, I've personally tended towards a smaller number of larger Pulpscript files with lots of logic inside, instead of spreading the code out over scripts for multiple rooms and items (which would require more copying and pasting).
Tip 2: Generating Pulpscript via other scripting
Less generally applicable than the first tip, but this helps when you know exactly what code you want to write, and it's just a matter of churning it out.
If you're familiar with another programming language, consider using it to print out large blocks of Pulpscript, and paste that into the Pulp editor. Most languages have string formatting functions, and you could maybe even do this in Pulpscript itself, using log statements and copying the output from the console!
I used Python below since it's simple and readable (and pre-installed on Macs, just open up the Terminal application and enter "python3").
You can copy and paste the below code into a Python script (or Python commandline) and it should print out the same Pulpscript event handler code blob as the above Tic-tac-toe example.
UNDO_LIMIT = 4
for k in range(0,UNDO_LIMIT):
print(f'on setStateT{k}ToCurrent do')
print(f' // log "@setStateT{k}ToCurrent"')
print(f' stateT{k}_player_x = playerStateLatest_x')
print(f' stateT{k}_player_y = playerStateLatest_y')
for i in range(0,3):
for j in range(0,3):
print(f' stateT{k}_x_{i}_y_{j} = name {i},{j}')
print(f'end')
print()
for k in range(0,UNDO_LIMIT):
print(f'on setCurrentToT{k} do')
print(f' // log "@setCurrentToT{k}"')
print(f' goto stateT{k}_player_x,stateT{k}_player_y')
print(f' playerStateLatest_x = stateT{k}_player_x')
print(f' playerStateLatest_y = stateT{k}_player_y')
for i in range(0,3):
for j in range(0,3):
print(f' tell {i},{j} to')
print(f' swap stateT{k}_x_{i}_y_{j}')
print(f' end')
print(f'end')
print()
Note that depending on what language you use, you may need to "escape" certain characters. For example in Python formatted strings, the brackets {} in the strings are used to insert Python code (the variables i
, j
, k
in this case). So if I wanted to print literal bracket characters in my result Pulpscript (maybe I want to log
some Pulpscript variable's value), in the Python code I'd have to use double brackets like {{this}}
.
It might take a couple tries to get the formatting just how you like it (tabs and spaces and indentation) but with this we can generate the necessary Pulpscript all the way up to whatever UNDO_LIMIT we want to set. Just remember that your Pulpscript code size (and result .json and .pdx files) will grow as well!
Finally, these numbered events can be called from somewhere else using a variable to insert a number (like Shaun describes here).
// player script, with so much omitted for brevity that this is almost pseudocode
on update do
playerStateLatest_x = event.px
playerStateLatest_y = event.py
call "setStateT{timeVar}ToCurrent"
timeVar++
end
on cancel do
log "undoing now"
timeVar--
if canUndo==1 then
call "setCurrentToT{timeVar}"
else
log "can't undo any further!"
end
(There was a lot more code needed for the full undo implementation, but that's a story for another time. EDIT: it is now another time, see here)
That's all I've got for now, hope someone found this helpful. Feel free to reply with your own workflow tips!