Cake Quest.exe: Postmortem

Cake Quest launched on itch.io and on the Lexaloffle BBS almost five months ago. At the time, I didn’t feel capable of putting my thoughts and feelings into words. This post serves as my best attempt to do that while reflecting on the process now that I’m no longer involved in its documentation.

If you have been following along with that documentation, then it’s no secret that the game is a culmination of a series of firsts. At its inception, it was my first PICO-8 project. Its initial lines of code were the first I wrote in Lua. It is the first game I’ve finished and released outside of a jam setting, making it the first to which I’ve dedicated more than a few weeks of my time.

In the first post discussing the jam concept that evolved into the PICO-8 game, I mentioned that it floundered partially because I was overwhelmed by how much I potentially needed to do and doubted my ability to actually do it. I’ve felt that way toward game development for the past few years, so Cake Quest was as much a fight against that as it was a learning experience and an opportunity to leave my comfort zone.

Like the jam games I’ve released, it was a solo project, and I was not financially reliant on its performance. This granted me the privilege of having no set deadline, allowing me to focus on each aspect of development for as long as I wanted. To me, a deadline was an expectation. I didn’t need the guilt associated with a failure to meet it.

Quantifying my work has always been difficult, if not impossible. If I were asked how long it would take me to complete a task, I would struggle to give an answer. Over the years, I’ve been told I’m proficient at and with a number of things, but procrastination is something at which I’ve excelled, thanks to near-consistent positive reinforcement in spite of its undeniably negative impact on my health.

Cake Quest‘s development may not have been procrastination-free, but I was determined not to let myself suffer for its sake. I took frequent breaks. On the days I didn’t feel like working on a specific task, I either worked on something else or didn’t work on anything. It was important not to force progress. I told myself that productivity was a switch that I was allowed to turn off and that I wasn’t lazy or unmotivated for doing so.

This approach made each “active” day more enjoyable. Tasks and problems that would otherwise have been obstacles to things I’d have rather been doing were instead puzzles I looked forward to solving. I mentioned before that the project was a learning experience. As those go, it was probably the best one I could have had, since most of these tasks and problems were entirely new to me. I not only gained valuable experience in devising my own solutions, but I also learned how to read and understand others’ solutions whenever I needed a push in the right direction. Considering PICO-8’s limitations, being aware of my specific needs and of what would (or would not) help me meet them was paramount.

There were certain quality-of-life things I avoided doing because they weren’t absolutely necessary, and the game was designed around their absence. I knew this decision would affect the game’s reception, and I justified it by telling myself that I wasn’t making the next Mario or Celeste. Regardless, I could have made more of an effort to get feedback, which I received about two weeks before launch. Someone saw one of my last progress GIFs and pointed out an issue with the camera behavior, specifically with its vertical motion.

I was both thankful and annoyed. I was thankful because the individual was respectful and undemanding. I was annoyed at myself for not prioritizing something like camera behavior from the outset because that made it harder to adjust or fix without adjusting other parts of the game, such as the level design. I wasn’t willing to make those sorts of adjustments. By that point, I was just looking forward to being done.

Even so, I spent the next week researching possible solutions. I was unsuccessful, but after the game launched, I remembered coming across a compilation of math-related Game Developers Conference presentations. From Squirrel Eiserloh’s presentation, “Juicing your Cameras with Math,” I learned about asymptotic averaging, which I was then able to implement with relatively little hassle. I can’t explain it as well as he can, so I’ll just say it improved the camera’s behavior, and I think the game is better for it.

The launch itself more or less went well. People liked how the game looked and complimented its itch.io page, which uses a built-in font and a custom background (I recreated one of the game’s sprites in Asesprite). Like my other games, it is fully playable in the browser. It is, however, my first to have downloadable options because I discovered that PICO-8 can export Mac, Windows, and Linux executables. I didn’t expect to get any downloads, but I thought it’d be useful for future projects.

The lesson I learned from including these is that I should have figured out how to use Butler first, which would have allowed me to update the executables without having to delete and re-upload them (like I did after the aforementioned camera adjustment). Since analytics don’t carry over between files, the number of downloads may have been impacted, but I can’t be sure.

What I am sure of is that I achieved what I had set out to do. When I began, I wasn’t confident. I was apprehensive. Now, I am relieved. Empowered. I’ve been reminded of some capabilities and have become aware of others. I am less discouraged and less intimidated by my limits. Most importantly, I am grateful for the encouragement and support I’ve received over the past year from my friends and fellow developers, without which I probably wouldn’t have gained the confidence to share the game.

Overall, this project was validating and a lot of fun to complete. It still involved a fair amount of work, but I don’t regret a second of it. 

The Quest to Make Cake Quest_part9.p8

The Key to Progression: Double Jumping Challenge (Days 19 and 20)

The design of every challenge begins with knowing that I’m going to put players at risk. Depending on their placement, extra lives can help prepare players for that risk or reward them for overcoming it. It’s not necessarily a binary choice, of course, but neither is it necessarily set in stone. It just needs to be made for the sake of progress—at least, it did after implementing Satan’s double jump.

The associated challenge obviously didn’t exist, so my choice was to reward players for completing the spike challenge. A series of platforms winds upward, and the life is at the top. About a week ago, I was reminded of the importance of making important objects visible to players thanks to Jools Watsham (of Atooi):

In the majority of cases, I’ve followed this principle, but there are three “blind jumps” in the game:

The first one is at the very beginning, the second one connects the top half of the spike challenge to the corridor below, and the third one connects the end of the corridor to the bottom half of the spike challenge. I felt justified in their inclusion because in all three cases, I did my best to keep players out of (unexpected) danger. I was originally going to include a fourth such jump down to the platform that begins the double jumping challenge, but it placed players at greater risk of losing a life without being able to react:

cakequest_19

What the second and third jumps have in common is that they restrict Satan’s horizontal movement. Adding more platforms accomplishes this for the fourth jump and makes the descent safer:cakequest_11

When designing the previous two challenges, I had the locations of their keys in mind and worked my way backward. This challenge was unique because the remaining space gave me the opportunity to place the key on either the far left side or the far right side of the map. Logistically, the key and its corresponding door would need to be on the same side, relegating the rock that grants Satan the double jump (the lemon rock) to the opposite side.

If the rock and key were on the same side, then players could unlock the door without ever seeing it (since it disappears). If the rock and door were on the same side, then players would have to cross the entire map twice: once to obtain the key and again to return to the unlocked door. That would have been feasible had I not dismissed the idea outright. A game’s length may inform the perception of its value in some circles, but I don’t consider it to be significant. That said, there are circumstances under which I would consider increasing it, such as wanting to present players with more difficult challenges or to further explore narrative ideas and concepts introduced earlier. Requiring players to collect something in order to finish the game is not one such circumstance.

The platform on which Satan lands is the long platform in the fourth image (click/tap on an image to see a larger version). As you can see, it is closer to the right side of the map than it is to the left side. I realized that if I placed the door and key on the right side, then part of the challenge could involve getting to the double jump rock on the left side. My next objective, then, was twofold. I needed to incentivize going left and to create opportunities to use the double jump once it was obtained. Through experimentation, I found a distance between platforms that couldn’t be crossed by a normal jump but that could be crossed by a double jump. Each of the platforms to the right of the longer platform is separated by this distance.

If it were ever in doubt that nothing is certain in game development, I cleared that up by immediately clarifying one of my earlier statements. I found a distance between platforms that couldn’t be crossed by a normal jump without stopping to rest. I removed some spikes between a few of the platforms leading to the left side of the map to give Satan access to the floor. Each of these spaces is 32 pixels wide (i.e. twice Satan’s width), giving Satan a buffer of eight pixels on either side between himself and the nearest spike. From the floor, players need to jump onto the next platform without touching the spikes underneath it, so this buffer helps with positioning:

cakequest_12

The pairs of spikes to the left of some of the platforms are there to prevent players from jumping from the floor onto those platforms without the double jump. If they try, Satan comes in contact with the spikes and respawns on the long platform. To compensate for this possibility, there is an extra life above the lemon rock. Its location is an indicator that Satan can now jump higher once he transforms the rock and eats the slice of cake it becomes:

cakequest_0

The return trip to the long platform serves as an opportunity for players to get accustomed to Satan’s increased movement range, both horizontally and vertically, so that when they encounter the final five platforms, the rest of the challenge is hopefully a formality. It consists of two horizontal jumps, one vertical jump, and two more horizontal jumps. The spikes on the last two platforms serve as tests of players’ timing. If Satan touches those or lands on the spikes beneath the platforms, he is returned to the long platform.

I don’t think I ever explicitly mentioned it in earlier posts, but I envisioned the game ending with Satan’s escape from Hell. The only (sensible) way out is up, so going through the door transports Satan back to the starting area, where there are two platforms he can use to jump to freedom (at the moment it just restarts the game):

cakequest_1

Making the platform visible at the start with no clear means of reaching it (hopefully) makes it stand out in players’ minds and provides them with an incentive to investigate it when they return at the end.

Code Catch-up: Another Note

In previous posts, I mentioned plans to discuss additions to the door-related functions. After some consideration, I’ve decided against that and will also not be discussing any of the code related to the content of this post. It may seem like I’ve suddenly decided not to be transparent about my design process, but this isn’t the case. As I write this post, the gameplay is finished. There isn’t much I have to do before the game itself is finished and released. When it is, the code will be made publically available in its entirety.

 

The Quest to Make Cake Quest_part8.p8

Devising a Double Jump (Days 17 and 18)

When I first implemented Satan’s ability to jump, I dismissed the idea of implementing a double jump. I thought that it wouldn’t be interesting enough, that as the final ability in the game, the conclusion it would help provide would be unsatisfying to players. In other words, it wasn’t ideal. However, I mentioned in the last post that losing the project was an opportunity to reevaluate previous decisions I had made.

My choice to use PICO-8 had forced me to think pragmatically from the outset. I gained an awareness of just how little space I had after finishing the initial jumping challenge, and the spike challenge had been an exercise in making the most of that space. Limitations like this were what had appealed to me and had given me hope that I could finish this project.

With that in mind, my opposition to a double jump was unjustifiable. It was a self-imposed limitation that impeded any sort of progress because I kept looking for alternatives. I needed to change my mindset, so to facilitate and force that change, I moved all of the jump-related code into its own function:

FUNCTION HANDLE_JUMPING()

IF((BTNP(2) OR BTNP(4)) AND P.HAS_WINGS) THEN

IF IS_GROUNDED() THEN

IF(((SOLID_TILE(P.X,P.Y-1)==TRUE) OR (SOLID_TILE(P.X+15,P.Y-1)==TRUE)) OR (SOLID_TILE(P.X+7,P.Y-1)==TRUE)) THEN
P.DY=0
ELSE
P.DY-=10
END

END

END

END

Next, I thought about the best approach I could take to implement a functional double jump. In this case, the “best approach” meant something that I understood and that worked. I created two variables: p.double_jump_enabled and p.jump_count. I set the latter to zero, and for testing purposes, I set the former to true. My plan was to increment p.double_jump_enabled by one when the jump key was pressed. When its value was equal to two, jumping would be disabled until Satan touched the ground, at which point the value would reset to zero.

Since the game already had an area dedicated to jumping, I decided to use it as a testing environment for the double jump. I don’t have the code associated with my initial implementation, unfortunately. However, I had the foresight to make a GIF to use as a visual aid. The main takeaways from it are that I counted jumps improperly and that I didn’t specify the conditions under which Satan could and could not double jump, which is why he can jump through platforms:

cakequest_8

Beyond those, my main problem was trying to rely entirely on the handle_jumping() function. As is the case with handle_physics(), the function “handles” core functionality, while restrictions on the values used by the function are entrusted to other functions. In terms of the double jump, this core functionality refers to its height and to incrementing p.jump_count:

FUNCTION HANDLE_JUMPING()

IF((BTNP(2) OR BTNP(4)) AND P.HAS_WINGS) THEN

IF IS_GROUNDED() THEN

IF(((SOLID_TILE(P.X,P.Y-1)==TRUE) OR (SOLID_TILE(P.X+15,P.Y-1)==TRUE)) OR (SOLID_TILE(P.X+7,P.Y-1)==TRUE)) THEN
P.DY=0
ELSE
P.DY-=10
P.JUMP_COUNT+=1
END

ELSE

IF(P.DOUBLE_JUMP_ENABLED AND P.CAN_DOUBLE_JUMP) THEN

IF(P.JUMP_COUNT==1) THEN
P.DY=0
P.DY-=8
P.JUMP_COUNT+=1
END

END

END

END

END

The variable p.can_double_jump starts as false and is toggled in handle_physics() depending on whether Satan is stuck in the ceiling. If he is, then it is false, and if he isn’t, then it is true. The height of the jump (the value of p.dy) is slightly less than that of the regular jump. Satan has a fair amount of girth, and I didn’t want the double jump to look effortless.

Setting p.dy to zero just before the actual jump occurs is important because Satan is constantly affected by gravity. Without this step, the height of the second jump changes depending on Satan’s location in the air. In another game, that might be acceptable, but this one isn’t designed around that level of precision. Double jumps (as I understand them) defy gravity, so this allows Satan’s jump to do so.

Of course, this code only permits Satan to perform a single double jump because the value of p.jump_count never becomes less than or equal to one after the jump is performed. I solved the problem by making an addition to update_player() above the code that enables lives:

FUNCTION UPDATE_PLAYER()

IF(P.BOLT_COUNT<0) THEN
P.BOLT_COUNT=0
END
IF COLLIDE(P.X,P.Y,P.HITBOX,T.X,T.Y,T.HITBOX) THEN
P.HAS_TRIDENT=TRUE
END
IF COLLIDE(P.X,P.Y,P.HITBOX,CHC.X,CHC.Y,CHC.HITBOX) THEN
P.HAS_WINGS=TRUE
END
IF P.DOUBLE_JUMP_ENABLED THEN

IF(IS_GROUNDED()==TRUE) THEN
P.JUMP_COUNT=0
ELSE

IF(P.JUMP_COUNT>2) THEN
P.JUMP_COUNT=2
END

END

END

IF COLLIDE(P.X,P.Y,P.HITBOX,GVC.X,GVC.Y,GVC.HITBOX) THEN
P.LIVES_ENABLED=TRUE
P.LIVES=3
END

END

Checking whether p.double_jump_enabled is true might seem superfluous because of its perpetual true status, but it’s simply a preemptive time-saving measure. I’d have had to write the condition when I made the double jump a cake-given ability, and now it’s one less thing to worry about when I get to that point. The rest of the code fulfills my earlier objectives. The value of p.jump_count is reset once Satan touches the ground, and restricting the maximum value to two ensures that he can’t jump more than twice in the air (what matters is that it’s higher than one; two just makes sense because it’s a “double” jump).

Beyond determining whether a double jump was actually occurring, my goal for the second test was to get a visual indication of the height Satan gained when jumping. An increase in height increased the likelihood of coming in contact with (and getting stuck in) a ceiling. Mastery is the product of experimentation, so when players experimented with the double jump, I wanted to make sure that they didn’t end up in situations that halted their progress or punished them unnecessarily. I couldn’t guarantee it’d never happen, but I could attempt to minimize the risk.

My next post will discuss the how the double jump influenced the design of the final area of the game.

cakequest_9

The Quest to Make Cake Quest_part7.5.p8

Crisis Averted (Days 15 and 16)

A[nother] Second Chance

I’m not sure if I’ve explicitly mentioned it before, but part of what motivates me to make games is documenting the process through these posts. They help keep projects alive as records of my progress at specific points in time. Since this site partially serves as a repository, that isn’t necessarily surprising, but I never thought I’d end up using it as a resource. Without the screenshots, GIFs, and code snippets I’ve previously shared and discussed, recreating Cake Quest would have taken a lot longer than it ultimately did, especially since I’m not sure if I’d have made the attempt.

Losing the project was more of an opportunity than a setback. I could reevaluate decisions I had made and “refactor” code I had written. Some portions of the map received minor spacing adjustments to make traversing it easier:

cakequest_2
This is from the old build.
cakequest_4
This is from the new build.

With a few exceptions, changes to the code were organizational. The first of these exceptions was an addition to the draw_trident() function:

FUNCTION DRAW_TRIDENT()

IF NOT P.HAS_TRIDENT THEN
T.SPRITE=SPR(5,T.X,T.Y,2,2)
ELSE
T.HITBOX={X=0,Y=0,W=0,H=0}

END

END

Because the collision between the trident’s hitbox and Satan’s is what determines whether the trident has been collected, setting the hitbox’s dimensions to zero ensures the two hitboxes can’t collide again once p.has_trident is true. The next addition was to update_player(), an if statement that checks whether p.bolt_count is less than zero and sets it to zero if it is.

cakequest_5

This fixes a bug related to p.bolt_count. In order for a bolt to be fired, p.bolt_count must be zero (checked in can_fire()), ensuring that only one bolt is fired at a time. Its value increases by one whenever a bolt is fired and decreases by one whenever a bolt hits a wall or rock. However, it also decreases when a bolt is a certain distance away from Satan in either direction. Consequently, if a bolt collides with a wall or rock at that distance, then the value of p.bolt_count becomes -1, disabling the ability to fire bolts and preventing players from progressing beyond the jumping challenge.

The last significant change to the code was the removal of the update_flags() function. In part 6, I said that despite no longer being drawn, doors were treated as solid objects because of the flag assigned to their sprites. While recreating the map, I realized that statement was slightly inaccurate. Each door originally replaced a section of the wall. The wall’s sprites are also flagged as solid objects, and these sections were intact in the map data, only getting replaced at runtime. All I needed to do was open the map editor and remove the wall sprites from where I wanted doors to be. Satan could then progress immediately after the door sprites were replaced with empty ones (and now he can).

The Nature of Life and Death

Creating Objects

Implementing the behavior of spikes started by once again duplicating the solid_tile() function and changing the flag:

FUNCTION SPIKE_TILE(X,Y)
LOCAL TILEX=FLR(X/8)
LOCAL TILEY=FLR(Y/8)

IF(FGET(MGET(TILEX,TILEY),4) THEN
RETURN TRUE
ELSE
RETURN FALSE
END

END

In the previous post, I mentioned that Satan couldn’t collide with spikes until he obtained lives. The table in make_table() needed two additional parameters: p.lives and p.lives_enabled. The former has a nil value by default, and the latter is set to false. Lives are enabled when Satan eats the green velvet cake. It’s identical to the devil’s food/chocolate cake aside from location:

FUNCTION MAKE_CAKE()
CHC={
X=224,
Y=384,
SPRITE=NIL,
HITBOX={X=4,Y=4,W=7,H=14}
}
GVC={
X=608,
Y=64,
SPRITE=NIL,
HITBOX=CHC.HITBOX
}

END

Its corresponding rock is also identical to the chocolate rock. It consists of four segments that are initially set to nil values and has a hit count of zero. While its hit count is less than two, it is drawn. Once its hit count is equal to two, then its segments are set to empty sprites (in update_grn_vlvt_rock()), and the cake is drawn in its place:

FUNCTION MAKE_ROCKS()
CR={
TOP_LEFT=NIL,
TOP_RIGHT=NIL,
BOTTOM_LEFT=NIL,
BOTTOM_RIGHT=NIL,
HIT_COUNT=0
}
GVR={
TOP_LEFT=NIL,
TOP_RIGHT=NIL,
BOTTOM_LEFT=NIL,
BOTTOM_RIGHT=NIL,
HIT_COUNT=0
}

END

FUNCTION DRAW_GRN_VLVT_ROCK()

IF(GVR.HIT_COUNT<2) THEN
GVR.TOP_LEFT=MSET(76,8,70)
GVR.TOP_RIGHT=MSET(77,8,71)
GVR.BOTTOM_LEFT=MSET(76,9,86)
GVR.BOTTOM_RIGHT=MSET(77,9,87)
ELSE
UPDATE_GRN_VLVT_ROCK()
DRAW_CAKE()
END

END

FUNCTION DRAW_CAKE()

IF NOT P.HAS_WINGS THEN
CHC.SPRITE=SPR(41,CHC.X,CHC.Y,2,2)
END
IF NOT P.LIVES_ENABLED THEN

IF(GVR.HIT_COUNT==2) THEN
GVC.SPRITE=SPR(9,GVC.X,GVC.Y,2,2)
END

END

END

As a reminder, the last two parameters in this implementation of spr() refer to width and height in sprite dimensions. Sprites are eight pixels high and eight pixels wide. A value of two sets the width and height to 16 pixels. Four sprites are drawn in total, with the sprite specified by number (i.e. the first parameter) comprising the top-left portion of the larger sprite.

There is currently only one extra life in the game. If that were to change, it would happen in make_extra_lives(). An extra life has a position, a sprite, a hitbox, and a collected status. It is drawn as long as that status is false:

FUNCTION MAKE_EXTRA_LIVES()
LIFE={
X=992,
Y=48,
SPRITE=NIL,
HITBOX={X=0,Y=0,W=8,H=8},
COLLECTED=FALSE
}
END

FUNCTION DRAW_EXTRA_LIVES()

IF NOT LIFE.COLLECTED THEN
LIFE.SPRITE=SPR(102,LIFE.X,LIFE.Y)
ELSE
LIFE.HITBOX={X=0,Y=0,W=0,H=0}
END

END

Collision-based Functionality

Everything associated with spikes and lives is handled in two main functions: update_player() and move_player(). In update_player(), once Satan’s hitbox collides with that of the green velvet cake, p.lives_enabled is set to true, and p.lives is set to three:

FUNCTION UPDATE_PLAYER()

IF(P.BOLT_COUNT<0) THEN
P.BOLT_COUNT=0
END
IF COLLIDE(P.X,P.Y,P.HITBOX,T.X,T.Y,T.HITBOX) THEN
P.HAS_TRIDENT=TRUE
END
IF COLLIDE(P.X,P.Y,P.HITBOX,CHC.X,CHC.Y,CHC.HITBOX) THEN
P.HAS_WINGS=TRUE
END
IF COLLIDE(P.X,P.Y,P.HITBOX,GVC.X,GVC.Y,GVC.HITBOX) THEN
P.LIVES_ENABLED=TRUE
P.LIVES=3
END

END

An addition to move_player() uses the status of p.lives_enabled to limit Satan’s movement without lives. Once lives are enabled, then a function named manage_lives() is called:

IF P.LIVES_ENABLED THEN
MANAGE_LIVES()
ELSE

IF(P.X>664) THEN
P.X=664
END

END

The manage_lives() function decreases the value of p.lives by one every time Satan collides with a spike. The set_position() function is called immediately afterward. It sets p.x and p.y to specific values depending on where Satan is when a collision occurs. If the total number of lives is equal to or below zero, then run() is called, resetting the game. Spike collisions don’t include a one-pixel buffer between sprites; this gives Satan more maneuverability (and players more path choices) during the spike challenge.

In order of appearance, the following checks refer to Satan’s top-left, top-center, top-right, center-left, bottom-left, bottom-center, bottom right, and center-right.

FUNCTION MANAGE_LIVES()

IF((SPIKE_TILE(P.X,P.Y)==TRUE) OR (SPIKE_TILE(P.X+7,P.Y)==TRUE) OR (SPIKE_TILE(P.X+15,P.Y)==TRUE) OR (SPIKE_TILE(P.X,P.Y+7)==TRUE) OR (SPIKE_TILE(P.X,P.Y+15)==TRUE) OR (SPIKE_TILE(P.X+7,P.Y+15)==TRUE) OR (SPIKE_TILE(P.X+15,P.Y+15)==TRUE) OR (SPIKE_TILE(P.X+15,P.Y+7)==TRUE) THEN

IF(P.LIVES>0) THEN
P.LIVES-=1
SET_POSITION()
ELSE
RUN()
END

END

END

FUNCTION SET_POSITION()

IF(P.Y<=200) THEN
--ON PLATFORM BEFORE FIRST SPIKES
P.X=656
P.Y=88

ELSEIF(P.Y>200 AND P.Y<350) THEN
--RIGHT OF GREEN DOOR
P.X=708
P.Y=344
END

END

cakequest_7

Since obtaining an extra life is the result of a hitbox-based collision, it gets added to the total in update_player():

IF(P.LIVES_ENABLED==TRUE) THEN

IF COLLIDE(P.X,P.Y,P.HITBOX,LIFE.X,LIFE.Y,LIFE.HITBOX) THEN
P.LIVES+=1
LIFE.COLLECTED=TRUE
END

END

Displaying Inventory

I figured that the simplest way to inform players of their total lives was to display that total on the screen. I wrote a function named draw_inventory() to accomplish this. In the original build, it only drew lives as they were gained or lost, but during one of many playthroughs, I decided to have the keys drawn as Satan collected them. The only other indication that a key has been collected is the disappearance of its corresponding door, but that’s not immediately apparent like a change to Satan’s sprite is, for example.

FUNCTION DRAW_INVENTORY()
CAMERA()

The call to camera() resets the camera offset to its default value (zero), ensuring that whatever is drawn afterward remains in a specific location on the screen regardless of the camera’s position.

IF P.LIVES_ENABLED THEN

IF(P.LIVES>=1) THEN
SPR(102,88,0)
END
IF(P.LIVES>=2) THEN
SPR(102,100,0)
END
IF(P.LIVES>=3) THEN
SPR(102,112,0)
END
IF(P.LIVES>=4) THEN
SPR(102,76,0)
END

END

Each of the four sprites is visible unless the value of p.lives falls below the specified number in each statement. For example, if Satan has four lives and then loses one, the fourth life is no longer visible, but the other three remain visible.

IF P.HAS_CHOC_KEY THEN
SPR(40,88,12)
END

IF P.HAS_GV_KEY THEN
SPR(55,100,12)
END

END

The “gv” in p.has_gv_key stands for “green velvet.” That key is created in make_keys(), drawn in draw_keys(), and is collected via hitbox collision in update_player(). Remember that the coordinates in make_keys() refer to the locations of both keys on the map, while the previous coordinates refer to where on the screen their sprites are drawn once they’ve been collected:

FUNCTION MAKE_KEYS()
CHOC={
X=248,
Y=64,
SPRITE=NIL,
HITBOX={X=0,Y=0,W=6,H=8}
GREEN={
X=944,
Y=200,
SPRITE=NIL,
HITBOX=CHOC.HITBOX
}

END

FUNCTION DRAW_KEYS()

IF NOT P.HAS_CHOC_KEY THEN
CHOC.SPRITE=SPR(40,CHOC.X,CHOC.Y)
END
IF NOT P.HAS_GV_KEY THEN
GREEN.SPRITE=SPR(55,GREEN.X,GREEN.Y)
END

END

IF COLLIDE(P.X,P.Y,P.HITBOX,CHOC.X,CHOC.Y,CHOC.HITBOX) THEN
P.HAS_CHOC_KEY=TRUE
END
IF COLLIDE(P.X,P.Y,P.HITBOX,GREEN.X,GREEN.Y,GREEN.HITBOX) THEN
P.HAS_GV_KEY=TRUE
END

Future Plans

Most of the code in the current build has been covered in this post. The only omissions are additions to the door-related functions, specifically relating to the green velvet door. I’ve decided that because I’ll be adding code related to the third door type to these functions, it’d be more appropriate to show and discuss both at the same time rather than in separate posts.

The Quest to Make Cake Quest_part7.p8

The Key to Progression: Spike Challenge (Days 12-14)

Creating Life in Hell

In many games, players are given health or lives as soon as a level loads. With them comes the expectation of potential loss upon contact with an enemy or object, but they’re otherwise taken for granted. Their impact on how players approach situations consequently tends to be minimal unless and until they’re almost gone. I wanted them to be meaningful as soon as they were introduced.

cakequest_010

Conveying that through appearance was simple enough. Shaded similarly to the trident and keys, the sprite is green because I decided Satan would earn lives by eating green velvet cake. The actual design was inspired by the miniature cakes and cupcakes sold under Hostess and other brands and is meant to be a non-specific devil or demon. I always thought characters collecting their own faces was a little weird, so I intentionally avoided basing it on Satan himself.

Pain without Suffering

It didn’t take long for spikes to become the designated hazard in this game. They were simple to draw, versatile (I could place them on floors, walls, ceilings, and platforms), and most importantly, they didn’t take up much space in the sprite sheet. The jumping challenge took up a significant portion of PICO-8’s map, and I quickly discovered that what looked like available space in the sprite sheet wasn’t. When I experimented with designs larger than 8×8, portions of those designs would appear in various places, mainly in walls and floors. This taught me the importance of making the most of limited space. To that end, I created two segments in addition to the four directional spike segments, allowing me to quickly and easily create spikes of any length.

Their challenge begins as an extension of the jumping challenge, but it doesn’t officially begin until Satan obtains lives by transforming the green velvet rock, which is placed in a recess above and to the left of a platform. To the right of that platform is a line of smaller platforms, and beneath these are several spikes. Without lives, Satan’s horizontal movement range is restricted to the end of the initial platform. Players can thus see the spikes but are unable to interact with them. Bolts can’t reach the green velvet rock unless Satan is in the air, forcing players to learn and master how to fire them while jumping. The movement restriction prevents players from accidentally falling into the spikes, and the platform’s longer length compared to the others helps players adjust to the new mechanic. More specifically, it reinforces the need for Satan to be moving before he can fire.

cakequest_008

As soon as Satan obtains lives, the movement restriction disappears. If he falls onto the spikes, his position is set to the initial platform. I wanted players to be penalized for losing lives, but I also wanted them to be able to start playing again immediately. If all lives are lost, then the game restarts:

cakequest_0

Players are offered a choice when they reach the fourth platform. They can either move down to the ground or head upward and attempt to get an extra life. There isn’t much to say about that upper section except that there were originally four spikes above the platform. I changed it when I realized that the reward wasn’t worth the risk. Getting on the platform without touching the spikes required pixel-perfect precision that players wouldn’t be prepared for, and even if they were, it was difficult to do reliably.* Reducing the number of spikes to three made it significantly easier to do, but it was still challenging (the below demonstration took three or four attempts):

cakequest_1

*Every time I make an addition or change, I play the game from the beginning up to the point where that addition or change is implemented. Not only does it help me experience the level as players will, but it also helps me make sure that the difficulty isn’t increasing too quickly from section to section. 

What Lies Beneath

Whether they get the extra life or not, players can only progress by jumping or falling down the spike-lined hole in the floor. If they head right, they see the green velvet cake key. At this point in the game, players would know that they needed to obtain it, so I used the key as a starting point for this section’s design. Its placement at the top of the section meant that players would need to work their way upward to reach it. Taking into account the limited space available (this was the second of three challenges, after all), I thought the best approach would be to create a maze or puzzle of sorts.

cakequest_009

I started by wondering how closely together platforms could be positioned with players still able to jump or move between them. The closer the platforms were, the more likely it was that players would get stuck in the bottoms of platforms above them. It then occurred to me that players could be encouraged to use that “ability” where they otherwise wouldn’t through spike placement. Furthermore, access to certain platforms could be restricted or prevented entirely, forcing players to make calculated decisions about which platforms will help them get to the key.

The spike placement in the above image was based on an arbitrary path I took involving the central platforms in each row. I intended it to be the only path, but after I happened to find another one, I realized that it was better if players had more than the illusion of choice. The prospect of them discovering paths I hadn’t planned or wasn’t aware of was exciting rather than a problem I needed to fix, with one exception. I designed the corridor at the top of the room mainly so that when players fell, the first thing they would see is the door leading to the next section of the level (this spot is also where they respawn upon losing a life). Its secondary purpose is to prevent players from falling on or near the top row of platforms and essentially skipping the challenge.

Code Catch-up: A Note

This isn’t how I intended to finish this post, but I’m committed to documenting everything that happens during the development of this game. A few days ago, I encountered an issue in which I wasn’t able to boot into my computer’s operating system. After trying every possible solution I could think of and find online, I erased the disk and reinstalled the operating system. I lost everything except for the screenshots and GIFs used here because I never created a backup (PICO-8’s automatic backup was deleted along with PICO-8).

As of this writing, I have recreated all of the sprites and redrawn the map. Publication of the next post will be delayed until I’ve rewritten the code. In that post, I’ll discuss the code relating to this post as well as any changes made to the game’s functionality or design (I’ve yet to test the redrawn map). If you are reading this, thank you for your patience and understanding.

The Quest to Make Cake Quest_part6.p8

Correction: In the previous post, I said that the parameters of mset() referred to pixel coordinates, but as I’ll discuss in this one, they actually refer to tile coordinates. 

The Key to Progression: Jumping Challenge (Days 10 and 11)

Design Decisions and Changes

Satan earning his wings meant that I had exhausted my list of definite mechanics. I didn’t have a clear idea of what to work on next, so I referred to the shorter list of potential mechanics. The first two were cake-focused (and therefore ability/attribute-focused), but the third, obtaining a “cake key”* to reach the level’s exit, required me to focus on level design. I’ve mentioned previously that I had been using the map solely as a testing environment up to this point. That would have to change sooner or later, so implementing this mechanic was my opportunity to enact that change.

As I thought about what Satan might have to do to obtain this key and how long it might take players to acquire, I toyed with the idea of having three keys instead of one, and the floodgates opened. Since I had three cake varieties, and each enhanced Satan in some way, I could design level sections based on those enhancements. The goal in each section could be to reach and obtain a key in order to unlock a door leading to the next section.

I tried to emulate the trident’s faux 3D appearance when designing the devil’s food/chocolate cake key. The key proper was the color of the cake’s frosting, and the shaded portions that created the 3D effect were the color of the cake. Since my carrot cake had white frosting, and my yellow cake had pink frosting, I couldn’t take this approach with their respective keys. PICO-8’s color palette was simply too limited. Rather than change the key design (much), I replaced the cake varieties. The carrot cake became green velvet, and the yellow cake became lemon:

cakequest_001
This was the original design.
cakequest_004
I stared at the original for too long while writing this, so this is the current design.

Keys are useless without doors, so I modified the unused designs I made earlier and colored them to reflect the new cake varieties. I also preemptively created the other two rock sprites:

cakequest_005

*Its name is a reference to the Japanese pronunciation of “cake.” 

Meddling with the Map Again

I decided to completely scrap the test environment rather than modify it. It was structured around my wants and needs as a designer. My approach to actual levels is to attempt to structure them around the wants and needs of players as much as possible.

The above images depict the first two sections of the game. Players begin in the top-left area. Its somewhat large size helps players become familiar with Satan’s ground movement and movement speed. Similarly, the drop down to the lower section introduces gravity and demonstrates how Satan behaves in the air. Once on the ground again, players encounter their first obstacle, the chocolate rock.* Going left, players can find and pick up the trident in a small room at the end of the corridor.

Because the trident adds another control option (shooting), its room and the connecting corridor give players the space to experiment with it before they test their new ability on the rock. This, in turn, ultimately grants them another new ability and access to the next section: the titular jumping challenge. I placed the sections close together for two reasons. The first was to conserve space, and the second was because I figured players would want to start jumping as soon as they were able.

Designing the jumping challenge was an iterative process. I started at the bottom of the room and worked upward. As a whole, the challenge is intended to be a safe, risk-free environment in which players can hone their skills. The blocks on the floor serve as a general introduction to the mechanic and help demonstrate Satan’s jump height. Above these is a row of platforms that teaches players the importance of positioning and timing when jumping horizontally. The platforms start out wide to allow for experimentation without heavily penalizing mistakes. If players fall when further up, they can also try to land on these to reduce the amount of climbing they have to do.

The first set of smaller platforms tests the ability to jump vertically. As they jump across to the second set and approach the right-most platform, players encounter the chocolate door and discover that it’s impassable, like the rock from the previous section. The remaining platforms point them in the direction of the key, which makes the door disappear upon being collected and grants players access to the next section:

cakequest_2
This challenge wasn’t explicitly designed to introduce getting stuck, but it will probably happen.

*It, the trident, keys, and doors don’t appear on the map until the game runs (since code handles their placement). The tiles used for the wall, floor, and ceiling are placed manually. 

Code Catch-up

Implementing the behavior of the key and door began with their respective make functions: make_keys() and make_doors(). Keys, like the trident, have an x-coordinate, a y-coordinate, a sprite, and a hitbox. Doors, like rocks, have sprites for each quadrant. Their only difference is the addition of a locked condition, which is true by default.

FUNCTION MAKE_KEYS()
CHOC={}
CHOC.X=250
CHOC.Y=48
CHOC.SPRITE=NIL
CHOC.HITBOX={X=1,Y=0,W=6,H=8}
END

FUNCTION MAKE_DOORS()
C_DOOR={}
C_DOOR.TOP_LEFT=NIL
C_DOOR.TOP_RIGHT=NIL
C_DOOR.BOTTOM_LEFT=NIL
C_DOOR.BOTTOM_RIGHT=NIL
C_DOOR.LOCKED=TRUE
END

Keys are drawn as long as they aren’t collected. In the case of the chocolate key, this depends on the status of an attribute in make_player() called p.has_choc_key:

FUNCTION DRAW_KEYS()

IF NOT P.HAS_CHOC_KEY THEN
CHOC.SPRITE=SPR(40,CHOC.X,CHOC.Y)
END

END

If the key is collected (i.e. if the player collides with it), then p.has_choc_key is true:

FUNCTION UPDATE_PLAYER()

IF COLLIDE(P.X,P.Y,P.HITBOX,T.X,T.Y,T.HITBOX) THEN
P.HAS_TRIDENT=TRUE
END
IF COLLIDE(P.X,P.Y,P.HITBOX,CHC.X,CHC.Y,CHC.HITBOX) THEN
P.HAS_WINGS=TRUE
END
IF COLLIDE(P.X,P.Y,P.HITBOX,CHOC.X,CHOC.Y,CHOC.HITBOX) THEN
P.HAS_CHOC_KEY=TRUE
END

END

This attribute doesn’t determine whether doors are drawn. Instead, it affects the status of their locked attribute:

FUNCTION DRAW_CHOC_DOOR()
C_DOOR.TOP_LEFT=MSET(72,11,68)
C_DOOR.TOP_RIGHT=MSET(73,11,69)
C_DOOR.BOTTOM_LEFT=MSET(72,12,84)
C_DOOR.BOTTOM_RIGHT=MSET(73,12,85)
END

The first two parameters are tile coordinates, and the third is a sprite number. Like pixel coordinates, tile coordinates increase from left to right and from top to bottom. Unlike pixel coordinates, values lower than zero are exclusively outside of the map/screen boundaries.

FUNCTION UPDATE_CHOC_DOOR()

IF P.HAS_CHOC_KEY THEN
C_DOOR.LOCKED=FALSE
C_DOOR.TOP_LEFT=MSET(72,11,0)
C_DOOR.TOP_RIGHT=MSET(73,11,0)
C_DOOR.BOTTOM_LEFT=MSET(72,12,0)
C_DOOR.BOTTOM_RIGHT=MSET(73,12,0)
UPDATE_FLAGS()
END

END

Each quarter of the door is replaced with an empty sprite when its corresponding key is collected, effectively making the door invisible. It’s still treated as a solid object because of the flag assigned to its sprites (0), so the update_flags() function disables the flag using fset():

FUNCTION UPDATE_FLAGS()

IF (C_DOOR.LOCKED==FALSE) THEN
FSET(68,0,FALSE)
FSET(69,0,FALSE)
FSET(84,0,FALSE)
FSET(85,0,FALSE)
END

END

As always, the final step was adding function calls to the game loop, but going forward, I’m not going to explicitly mention it unless the location isn’t clear from the function name. The draw_choc_door() and draw_keys() functions get called in _draw(), and update_choc_door() gets called in _update().

 

The Quest to Make Cake Quest_part5.p8

The Devil Gets His Wings (Day 9)

Designing a Bolt Target and Making It Disappear

While I enjoyed testing the added functionality of the physics I implemented, I couldn’t get too attached because I needed to restrict that functionality in the name of progress. The goal was for it to be enabled only once Satan gained his wings, after all. I had already established that he would get them by eating cake. Cake, of course, is what certain objects get turned into once they are hit by a bolt from Satan’s trident. The task at hand, then, was dual-faceted. I needed to determine exactly what Satan would be shooting, and I needed to figure out how to replace that object when it was hit.

cakequest_004

Originally, I thought that Satan might shoot at specific walls or “doors” that shared color schemes with specific cake varieties. These varieties were devil’s food (chocolate) cake, carrot cake, and yellow cake. Enemies had always been outside of the game’s scope because I considered it more important to get Satan’s movement and behavior working well than to implement and manage the movement and behavior of various enemy types. Ultimately, I replaced the walls/doors with rocks.* They were more versatile in terms of map placement, and I could create them with minimal changes to the existing sprites.

I decided that chocolate cake would give Satan his wings, so I focused my efforts on the chocolate rock. First, I duplicated the solid_tile() function and created a new function called choc_tile(). The only difference between the former and the latter was the number of the sprite flag for which it checked:

FUNCTION CHOC_TILE()
LOCAL TILEX=FLR(X/8)
LOCAL TILEY=FLR(Y/8)

IF FGET(MGET(TILEX,TILEY),1) THEN
RETURN TRUE
ELSE
RETURN FALSE
END

END

Just as a reminder, this converts x and y-coordinates from pixel (screen) coordinates to tile (map) coordinates and then checks to see if the sprite at that location has been assigned the specified flag. With that done, I then wrote a make_rocks() function, since it would be more efficient in the long run than writing a function for each rock variety:

FUNCTION MAKE_ROCKS()
CR={}
CR.TOP_LEFT=NIL
CR.TOP_RIGHT=NIL
CR.BOTTOM_LEFT=NIL
CR.BOTTOM_RIGHT=NIL
CR.HIT_COUNT=0
END

The first four attributes each refer to an 8×8 sprite that comprises 1/4 of the rock, and hit_count refers to how many times the rock has been hit by a bolt, which also determines whether or not it is drawn:

FUNCTION DRAW_CHOC_ROCK()

IF (CR.HIT_COUNT==0) THEN
CR.TOP_LEFT=MSET([X],[Y],66)
CR.TOP_RIGHT=MSET([X],[Y],67)
CR.BOTTOM_LEFT=MSET([X],[Y],82)
CR.BOTTOM_RIGHT=MSET([X],[Y],83)
ELSE
UPDATE_CHOC_ROCK()
END

END

I used the built-in mset() function to assign the proper sprite to each rock quadrant. Parameters [X] and [Y] represent explicit pixel coordinates that differ in the current build from what they were at the time this code was originally written. They’re based on values of p.x and p.y I wrote down while testing, and that testing environment no longer exists (I’ll address that in a future post). The last parameter in mset() is the sprite number in the sprite sheet. The update_choc_rock() function sets the sprite number of each quadrant to zero (an empty slot) and has no additional functionality or conditions that must be met, so I’m not showing it here.

As a final step before function calls and testing, I made some minor additions to the manage_bolt_mvmt() function:

FUNCTION MANAGE_BOLT_MVMT()

FOR B IN ALL(BOLTS) DO

IF B.RIGHT THEN

IF (SOLID_TILE(B.X+16,B.Y+7)==FALSE) THEN B.X+=B.SPEED
B.Y+=B.DY
ELSE
DEL(BOLTS,B)
P.BOLT_COUNT-=1
END
IF (CHOC_TILE(B.X+16,B.Y+7)==TRUE) then
DEL(BOLTS,B)
P.BOLT_COUNT-=1
CR.HIT_COUNT+=1
END

ELSEIF B.LEFT THEN

IF (SOLID_TILE(B.X-1,B.Y+7)==FALSE) THEN
B.X-=B.SPEED
B.Y-=B.DY
ELSE
DEL(BOLTS,B)
P.BOLT_COUNT-=1
END
IF (CHOC_TILE(B.X-1,B.Y+7)==TRUE) THEN
DEL(BOLTS,B)
P.BOLT_COUNT-=1
CR.HIT_COUNT+=1
END

END

IF (B.X>P.X+30 OR B.X<P.X-30) THEN
DEL(BOLTS,B)
P.BOLT_COUNT-=1
END

END

END

The function calls I’m referring to involve make_rocks() and draw_rocks(),** which are called in _init() and _draw(), respectively. However, I’d like to briefly discuss a third call I made earlier to the built-in camera() function. This call is the first line I have in _update(), and it looks like this:

CAMERA(P.X-64,P.Y-64)

It determines the amount by which the camera is offset. This offset (zero by default) is technically applied to the position of everything on the screen because the screen moves in relation to the camera. By subtracting 64 from p.x and p.y, I ensure that Satan is in the center of the screen at all times, giving the impression that the camera is following him as he moves.

Below is the result of running the game with the code written up to this point:

cakequest_3
I didn’t explicitly mention that the rocks were also solid, and now I don’t have to.

*I’d like to say that the decision was influenced by rock cake, but it’s just a thematically appropriate coincidence. 

**The draw_rocks() function calls each of the draw functions for the individual rocks so that I can (eventually) do in one line of code what would otherwise take three.

Have Your Cake and Eat It, Too

One of the perks of programming this game is that cake has fewer ingredients than it does in the physical world and thus takes less time to make:

FUNCTION MAKE_CAKE()
CHC={}
CHC.X=[X]
CHC.Y=[Y]
CHC.SPRITE=NIL
CHC.HITBOX={X=4,Y=4,W=7,H=14}

As in draw_choc_rock(), [X] and [Y] represent explicit pixel coordinates, specifically those of the rock. I admit that I could (and should) have come up with a better name for a cake variety than chc, but it wasn’t high enough on my list of development priorities. That honor went to drawing the cake, which I did by copying the draw_trident() function and substituting a few attributes:

FUNCTION DRAW_CAKE()

IF NOT P.HAS_WINGS THEN
CHC.SPRITE=SPR(41,CHC.X,CHC.Y,2,2)
END

END

I added p.has_wings to make_player() and set it to false. It may or may not have been mentioned previously, but the last two parameters in spr() refer to a sprite’s width and height, and the above if-not statement is an alternate way of writing the below if statement:

IF (P.HAS_WINGS==FALSE)

To make the rock appear to turn into a piece of cake, I needed to call this function immediately after the rock gets updated, and that happens in draw_choc_rock():

FUNCTION DRAW_CHOC_ROCK()

IF (CR.HIT_COUNT==0) THEN
CR.TOP_LEFT=MSET([X],[Y],66)
CR.TOP_RIGHT=MSET([X],[Y],67)
CR.BOTTOM_LEFT=MSET([X],[Y],82)
CR.BOTTOM_RIGHT=MSET([X],[Y],83)
ELSE
UPDATE_CHOC_ROCK()
DRAW_CAKE()
END

END

Actually giving Satan wings required two final steps. He needed to be able to interact with the piece of cake, and his sprite needed to change when that happened.

FUNCTION UPDATE_PLAYER()

IF COLLIDE(P.X,P.Y,P.HITBOX,T.X,T.Y,T.HITBOX) THEN
P.HAS_TRIDENT=TRUE
END
IF COLLIDE(P.X,P.Y,P.HITBOX,CHC.X,CHC.Y,CHC.HITBOX) THEN
P.HAS_WINGS=TRUE
END

END

I used a separate if statement because Satan’s wings are an addition to his sprite rather than a replacement.

FUNCTION DRAW_PLAYER()

IF (P.HAS_TRIDENT==TRUE) THEN

IF (P.HAS_WINGS==TRUE) THEN
--WINGED WITH TRIDENT

IF P.IDLE THEN
P.SPRITE=SPR(37,P.X,P.Y,2,2)
ELSEIF P.LEFT THEN
P.SPRITE=SPR(7,P.X,P.Y,2,2,TRUE)

ELSEIF P.RIGHT THEN
P.SPRITE=SPR(7,P.X,P.Y,2,2)
END

The “true” parameter in spr() flips the sprite horizontally.

ELSE
--WINGLESS WITH TRIDENT

IF P.IDLE THEN
P.SPRITE=SPR(33,P.X,P.Y,2,2)
ELSEIF P.LEFT THEN
P.SPRITE=(SPR,35,P.X,P.Y,2,2,TRUE)
ELSEIF P.RIGHT THEN
P.SPRITE=SPR(35,P.X,P.Y,2,2)
END

END

ELSE
--WINGLESS WITHOUT TRIDENT

IF P.IDLE THEN
P.SPRITE=SPR(1,P.X,P.Y,2,2)
ELSEIF P.LEFT THEN
P.SPRITE=SPR(3,P.X,P.Y,2,2,TRUE)
ELSEIF P.RIGHT THEN
P.SPRITE=SPR(3,P.X,P.Y,2,2)
END

END

END

 After adding a call to make_cake() in _init(), I was done. Satan finally looked and behaved as intended:

 

cakequest_4
He can’t fly, but I think getting cake makes up for it.

 

The Quest to Make Cake Quest_part4.p8

Fun with Physics (Days 7 and 8)

Writing the Main Function

A game’s physics, like its collisions, aren’t handled by any built-in functions in PICO-8. I wasn’t looking forward to tackling the challenge of their implementation, but I knew that progress on this game would stagnate if I didn’t. One of my definite mechanics involves the ability to jump, so I opted to focus on jump-related physics. This started with more time away from the computer to think about the information I needed to know and values I potentially needed to access.

I started by thinking about gravity. Its inclusion meant that the player was able to fall. If the player was able to fall, then I would have to determine whether the player was falling and, by extension, whether the player was on the ground or moving upward (i.e. jumping). I would need a jump height and a falling speed. Perhaps acceleration would be a factor. I considered all of these and others, knowing that not everything could or would make it into the game.

I wasn’t sure how best to organize whatever actually made it in, so most of it ended up contained within the following function:

FUNCTION HANDLE_PHYSICS()
GRAVITY=2

IF (P.DY <= 0) THEN
P.FALLING=FALSE
ELSE
P.FALLING=TRUE
END

I added p.falling and p.dY to make_player() before writing this function. By default, the former is false, and the latter is set to zero. In the previous post, I mentioned that dY refers to a change in y-position. I added a statement to the end of move_player() (where handle_physics() ultimately is called) that increases p.y by the value of p.dY. PICO-8’s on-screen y-coordinates increase from top to bottom, so if p.dY is positive, then p.y increases. Conversely, p.y decreases if p.dY is negative.

IF IS_GROUNDED() THEN
P.DY=0
P.Y=FLR(P.Y/8)*8
ELSE
P.DY+=GRAVITY

IF (P.DY > 4) THEN
P.DY=4
END

The line P.Y=FLR(P.Y/8)*8 is probably the most important line in this function. I covered flr() before when discussing sprite-to-wall collision, but what happens here is that p.y is converted from a pixel coordinate to a tile coordinate and then back to a pixel coordinate. The purpose of this is to prevent the player from landing between tiles. I set p.dY to a certain value if that value is exceeded (p.dY increases by two during every frame of falling) to prevent clipping through tiles, which occurs if the falling speed is too high.

--STUCK IN CEILING
IF (((SOLID_TILE(P.X,P.Y-1)==TRUE) AND (SOLID_TILE(P.X+15,P.Y-1)==TRUE)) OR (SOLID_TILE(P.X+7,P.Y-1)==TRUE)) THEN
P.DY=0
P.SPEED=0
P.FALLING=FALSE

IF(BTN(3)) THEN
P.DY+=.5
END

--FREED FROM CEILING
ELSE
P.SPEED=2
END

END

END

In the event that the player’s jump puts Satan in contact with the ceiling, I decided that he should get stuck in it. The points of contact are both of his horns or the center of the top of his head (well, one pixel above these). Setting the speed to zero disables horizontal movement, but he can still face left and right. The value by which p.dY increases is arbitrary, chosen only so that the transition from stuck to unstuck isn’t instantaneous.

Getting Grounded and Tweaking Bolts

The is_grounded() function is the polar opposite to handle_physics() in terms of complexity. It determines whether there is a solid tile (i.e. ground) one pixel below Satan’s bottom-left or bottom-right. If there is one, it returns true. Otherwise, it returns false:

FUNCTION IS_GROUNDED()

IF ((SOLID_TILE(P.X,P.Y+16)==TRUE) OR (SOLID_TILE(P.X+15,P.Y+16)==TRUE)) THEN
RETURN TRUE
ELSE
RETURN FALSE
END

END

The trident’s ability to fire bolts is not jump-related, but it is progression-related. My plan is for Satan to encounter the trident before he gains the ability to jump, so I thought it was important to address the firing rate. I added an attribute to the player table called p.bolt_count, which is zero by default. I then used it in a function I wrote called can_fire(), which returns true if the bolt count is zero and returns false if it is not:

FUNCTION CAN_FIRE()

IF (P.BOLT_COUNT==0) THEN
RETURN TRUE
ELSE
RETURN FALSE
END

END

Jump Up, Super Star

IF (BTNP(2) OR BTNP(4)) THEN

IF IS_GROUNDED() THEN

IF (((SOLID_TILE(P.X,P.Y-1)==TRUE) OR (SOLID_TILE(P.X+15,P.Y-1)==TRUE)) OR (SOLID_TILE(P.X+7,P.Y-1)==TRUE)) THEN
P.DY=0
ELSE
P.DY-=10
END

END

END

Jumping is a form of movement, so I wrote this code in move_player(). Buttons two and four correspond to the up arrow key and the Z key, respectively. I included a choice between them in case players who wanted to fire bolts while in the air weren’t comfortable with one or the other. The actual jumping is handled by the line P.DY-=10. Checking to see if Satan’s on the ground first prevents him from reaching and getting stuck in areas he shouldn’t have access to, while the solid_tile() checks fix a bug I encountered.

If there was a solid tile (ceiling) directly above Satan when he jumped, he’d clip through it because its location was less than the jump height. If the tile he clipped through was part of a wall, then he’d get stuck 10 pixels above that tile because at least one wall tile acted as a ceiling. I could get him out because of what I wrote in handle_physics(), but he’d be unable to move once he landed on the ground. My guess was that the distance between the ceiling and the ground was too small for him to be considered unstuck from the ceiling. My solution was to set p.dY to zero under those circumstances, which effectively disables jumping.

There are some minor things I could cover here about camera control, function calls, and code organization, but I’ll save that for my next post. Until then, I’ll show you these physics in action:


On the left is my initial physics test, and on the right is my second test. I think, aside from fire rate testing, the only major difference in the second test is that p.dY‘s maximum value is six instead of four to give a more noticeable speed increase.

 

The Quest to Make Cake Quest_part3.p8

Playing with Projectiles (Day 6)

Creation and Behavior

In the list of definite mechanics, which I mentioned in the previous post, Satan is able to fire lightning from the trident while moving. To accomplish this, I wrote three functions: make_bolts()fire_bolts(), and manage_bolt_mvmt().

FUNCTION MAKE_BOLTS()
BOLTS={}
END

FUNCTION FIRE_BOLTS()
LOCAL B={
SPRITE=39,
X=P.X,
Y=P.Y,
SPEED=5,
DY=0,
LEFT=FALSE,
RIGHT=FALSE,
HITBOX={X=0,Y=1,W=8,H=6}
}

ADD(BOLTS,B)

IF P.LEFT THEN
B.LEFT=TRUE
B.RIGHT=FALSE
ELSEIF P.RIGHT THEN
B.LEFT=FALSE
B.RIGHT=TRUE
END

END

The fire_bolts() function populates the table created in make_bolts() with bolts that have the specified attributes. You may notice a difference in how the local b table is instantiated compared to how tables are instantiated in previous functions. My understanding is that unless otherwise specified, PICO-8 treats all variables as global. As a result, if I instantiated b (which is local) as an empty table and then instantiated a variable as b.name, where name could be anything, then calling fire_bolts() would result in an error because b.name would be interpreted as an attribute of a global b, which doesn’t exist. I imagine the solution to that would be to explicitly declare each of the attributes as local, but that would also negatively affect the code’s readability and be needlessly repetitive. The b.dY attribute refers to a change in y-position. It starts at zero because I don’t want there to be any change, but I might want it later (which is why I bothered to create it in the first place).

FUNCTION MANAGE_BOLT_MVMT()

FOR B IN ALL(BOLTS) DO

IF B.RIGHT THEN

IF(SOLID_TILE(B.X+16,B.Y+7)==FALSE) THEN
B.X+=B.SPEED
B.Y+=B.DY
ELSE
DEL(BOLTS,B)
END

ELSEIF B.LEFT THEN

IF(SOLID_TILE(B.X-1,,B.Y+7)==FALSE) THEN
B.X-=B.SPEED
B.Y-=B.DY
ELSE
DEL(BOLTS,B)
END

END

IF(B.X>P.X+30 OR B.X<P.X-30) THEN
DEL(BOLTS,B)
END

END

END

Just in case it wasn’t clear earlier, this function manages bolt movement. The statement at the top is Lua’s version of a “for loop.” It iterates through the bolts table, and once it finds a bolt (b), it performs an action on that bolt. In this case, bolts are assigned either a positive or negative speed based on their direction, which is based on the player’s position and is determined in fire_bolts(). A positive speed makes a bolt move to the right, and a negative speed makes a bolt move to the left. If it hits a wall, the bolt is deleted from the table, hence the call to del(), and it disappears. The statement at the end of the function checks whether a bolt has moved 30 pixels away from the player in either direction and removes it from the bolts table if it has. The specific distance was decided arbitrarily. My goal was to prevent bolts from traveling indefinitely when unobstructed.*

Drawing and Function Calls

The bolts needed to appear on-screen and actually be fired before I could test whether they behaved as I expected. To accomplish the former, I created a draw_bolts() function:

FUNCTION DRAW_BOLTS()

FOR B IN ALL(BOLTS) DO

IF B.LEFT THEN
SPR(B.SPRITE,B.X+2,B.Y+5,1,1,TRUE)
ELSEIF B.RIGHT THEN
SPR(B.SPRITE,B.X+5,B.Y+5)
END

END

END

 The bolt’s sprite is assigned based on its direction. If it is facing to the left, the sprite is flipped because the sprite faces to the right in the sprite sheet. The position of each bolt is in relation to the end of the trident, since that is the object that fires them. I specified that in a slight addition to move_player(), which I also reorganized:

FUNCTION MOVE_PLAYER()
P.IDLE=TRUE

IF(BTN(0)) THEN
P.IDLE=FALSE
P.RIGHT=FALSE
P.LEFT=TRUE

IF(SOLID_TILE(P.X-1,P.Y)==FALSE) THEN
P.STOPPED=FALSE
P.X-=P.SPEED
ELSE
P.STOPPED=TRUE
END

END

IF(BTN(1)) THEN
P.IDLE=FALSE
P.LEFT=FALSE
P.RIGHT=TRUE

IF(SOLID_TILE(P.X+16,P.Y)==FALSE) THEN
P.STOPPED=FALSE
P.X+=P.SPEED
ELSE
P.STOPPED=TRUE
END

END

IF(BTNP(5) AND P.HAS_TRIDENT==TRUE) THEN

IF NOT P.IDLE AND NOT P.STOPPED THEN
FIRE_BOLTS()
END

END

END

The most important change from the previous version of the function is the addition of the p.stopped variable, which I added to make_player() after I realized I didn’t want bolts to fire if the player is up against a wall. The other one involved replacing the if-elseif statement structure with just if statements in order to make future changes (i.e. additional functionality) easier to implement. My immediate focus, however, was on adding function calls where appropriate so I could finally start testing. The fire_bolts() function was taken care of, so I only needed to worry about make_bolts()draw_bolts(), and manage_bolt_mvmt(). I added a call to the first in _init(), a call to the second in _draw(), and a call to the third in _update().

cakequest_0
He knows an update will reduce the fire rate, so he’s enjoying himself while he can.

*This hadn’t yet been implemented at the time the GIF was created, but it was added shortly enough afterward and was so minor an addition that I thought it best to mention it here.

The Quest to Make Cake Quest_part2.p8

First Game Mechanics and Additional Sprites (Day 3)

Cake Quest, at its core, consists of four main elements: Satan, cake, a trident, and lightning. My version of the game now had the first. I took some time away from the computer to think about how they would all interact with one another and devised two lists. The first list focused on what I called definite mechanics, while the second list focused on what I called potential mechanics. Both types were rules that would govern and influence the structure of the game. The words definite and potential referred not only to the likelihood of inclusion but also to whether there was a clear plan for implementation.

Definite Mechanics

  • Satan starts without the trident.
  • The trident is a collectible that must be picked up. Once it is picked up, Satan can fire lightning projectiles from it. He cannot put it down.
  • Projectiles are unlimited and can only be fired if Satan is facing left or right.
  • Cake is also a collectible. It appears as slices and has several varieties. Each variety imbues Satan with a different ability or attribute.
  • One variety gives Satan a pair of wings. With wings, Satan cannot fly, but he gains the ability to jump.
  • Objects that can be turned into cake use the same color scheme as a given cake variety.

Potential Mechanics

  • Satan earns new abilities or attributes from a single slice of cake. Alternatively, he only earns a new ability or attribute after eating a certain number of slices.
  • Cake could give Satan health or lives. There could even be some variety or varieties that harm(s) him.
  • Satan must find and use a “cake key” to open a door leading to the level’s exit.

According to Plan

The mechanics were ordered arbitrarily, but the sprites associated with them were not. I started with the trident because its design would influence the appearance of Satan’s other sprites. Initially, the dark blue that gives it a three-dimensional appearance was not present. I added it after I realized that it would help the trident stand out more from the features of Satan’s body, and in-game, I thought it might accentuate its status as an important object. To create Satan’s other sprites, I just duplicated and slightly modified the profile and idle sprites. The “lightning” looks like a fireball or something similar because I couldn’t figure out how to make a recognizable, stereotypical bolt with the limited space I had.* The pieces of cake went through three previous iterations (see below) and therefore took the longest to make.

cakequest_000

*It needed to be big enough to be noticeable but smaller than Satan’s 16×16 sprites. It is 8 pixels long and six pixels wide. 

Drawing the Map and Collision Testing (Days 4 and 5)

Meddling with the Map

It’s difficult to draw a map if there isn’t anything to draw, so after I finished the major sprites, I created a map tile and used it to create a simple room:

cakequest_002

Using the map editor is as simple as selecting a sprite and then clicking on one of the cells to place the sprite into the cell. Each cell, like the slots in the sprite sheet, is 8×8, and the map editor, unlike the sprite editor, doesn’t have an option to adjust the zoom level. Instead, I simply had to hold the Shift key and then drag the mouse over the sprite to increase the selection size (it also works in the sprite editor, but I wasn’t aware of it when I was first drawing sprites).

Drawing the map in the editor doesn’t display the map on the screen. That needs to be done in code via the map() function. I opened another tab and placed a call to map() into its own function, appropriately titled draw_map(), and a call to that function in _draw():

FUNCTION DRAW_MAP()
MAP(0,0,0,0)
END

FUNCTION _DRAW()
CLS()
DRAW_MAP()
DRAW_PLAYER()
END

The parameters of the map() function aren’t very informative on their own. The first two refer to the x and y-coordinates of a cell’s position in the map, and the second two refer to the x and y-coordinates of a pixel’s position on the screen. In this case, these coordinates correspond to the top-left corner of the map and to the top-left corner of the screen, respectively. The result of running the code is the following:

cakequest_001

Admittedly, I wasn’t paying much attention to the cell coordinates of the map when drawing this room, so this was accidental. The room’s not perfectly square, but that’s easily fixed and not a priority for the purposes of this update.

Sprite-to-Wall Collision

PICO-8 doesn’t have any built-in functions to handle collisions, so if I wanted them, I had to write my own. To do that, I needed some information, and to get that information, I needed to write a function to display it. That function was debug(), and it looked like this:

FUNCTION DEBUG()
PRINT("PIXELX: "..P.X,0,0,7)
PRINT("PIXELY: "..P.Y,0,6,7)
PRINT("TILEX: "..FLR(P.X/8),0,12,7)
PRINT("TILEY: "..FLR(P.Y/8),0,18,7)
END

It is a modified version of a function used in Uncle Matt’s PICO-8 tutorial series. The print() function that it calls is built-in and prints a string (a sequence of characters placed within quotation marks), displaying it on the screen. The .. is used in place of the plus sign in other programming languages to add whatever comes after it to the end of the string, an action called concatenation.

From top to bottom, pixelX is the player’s x-position in pixels, pixelY is the player’s y-position in pixels, tileX is the player’s x-position in (map) tiles, and tileY is the player’s y-position in (map) tiles. Each tile is 8×8, so converting pixels to tiles requires division by eight. The flr() function (an abbreviation of floor) is a built-in math function. It returns the closest integer value equal to or below what it is given. For example, flr(1/8) returns zero and not .125.

The next two parameters in print() indicate where each string should be printed on the screen (the coordinate (0, 0) is the top-left corner), and the last parameter refers to the index of a color in the sprite editor’s color palette (seven is white). I don’t have a visual aid, but if I were to run the game with the code written at this point (debug() gets called in _draw()), pixelX would be 40, pixelY would be 80, tileX would be 5, and tileY would be 10.

In the game’s current state, if I were to move Satan toward either wall of the room I had created, he would pass through it and eventually go beyond the boundaries of the screen. My next objective was to use the information I now had, specifically tileX and tileY, to prevent him from passing through the walls.

cakequest_004

Referring back to the sprite editor, below the slider that controls the zoom level is a series of circles. Each of these is a “flag.” The leftmost flag is zero, and the rightmost flag is seven. By default, they are all inactive. Activating them is as simple as clicking on them, and when active, they change color. In the above image, flag zero is active, and its color is red. For the purposes of wall collision, I needed a function that would check to see if a map tile (more specifically, the sprite associated with that tile) had an active flag zero. If it did, that tile was considered “solid” and therefore impassible. If it didn’t, then Satan could move unimpeded:

FUNCTION SOLID_TILE(X,Y)
LOCAL TILEX=FLR(X/8)
LOCAL TILEY=FLR(Y/8)

IF FGET(MGET(TILEX,TILEY),0) THEN
RETURN TRUE
ELSE
RETURN FALSE
END

END

This function is also adapted from the aforementioned tutorial series. The local keyword establishes these tileX and tileY values as local variables, which means they are only accessible by this solid_tile() function. The fget() and mget() functions are built-in. The “f” stands for “flag,” and the “m” stands for “map.” Given a position on the map (tileX and tileY), mget() returns the number of the sprite associated with the tile in that position, and fget() returns either true or false depending on whether the specified flag is active.

To implement this function, I needed to call it in move_player() to be able to check collisions:

FUNCTION MOVE_PLAYER()

IF(BTN(0)) THEN
P.IDLE=FALSE
P.RIGHT=FALSE
P.LEFT=TRUE

IF(SOLID_TILE(P.X-1,P.Y)==FALSE) THEN
P.X-=P.SPEED
END

ELSEIF(BTN(1)) THEN
P.IDLE=FALSE
P.LEFT=FALSE
P.RIGHT=TRUE

IF(SOLID_TILE(P.X+16,P.Y)==FALSE) THEN
P.X+=P.SPEED
END

ELSE
P.IDLE=TRUE
END

END

The values of p.x and p.y are the player’s screen position, but on the sprite, they are the coordinates of the top-left corner. The adjustments P.X-1 and P.X+16 provide a buffer of one pixel on either side of the sprite that stops Satan’s movement just outside of the wall tiles:

cakequest_0

Sprite-to-Sprite Collision

I remembered from the third PICO-8 fanzine that one approach to collision detection between sprites involved hitboxes. Perhaps most familiar to fans of fighting games, hitboxes are invisible areas around a sprite or model, often in the shape of a box (in 2D) or a cube (in 3D). If the hitbox around one object intersects the hitbox around another object, those two objects are considered to have collided. In the fanzine, hitboxes are defined as tables with x, y, width, and height values, where x and y are the coordinates of the top-left corner of the hitbox:

P.HITBOX={X=4,Y=7,W=7,H=6}

cakequest_005
A visual representation of the hitbox defined above.

I added the above definition of p.hitbox to the player table in make_player(). There are several approaches to hitbox collision in PICO-8, but the most straightforward I found was from a template by Brian Vaughn (@morningtoast):

FUNCTION COLLIDE(X1,Y1,HITBOX1,X2,Y2,HITBOX2)
LOCAL LEFT=MAX(X1+HITBOX1.X,X2+HITBOX2.X)
LOCAL RIGHT=MIN(X1+HITBOX1.X+HITBOX1.W,X2+HITBOX2.X+HITBOX2.W)
LOCAL TOP=MAX(Y1+HITBOX1.Y,Y2+HITBOX2.Y)
LOCAL BOTTOM=MIN(Y1+HITBOX1.Y+HITBOX1.H,Y2+HITBOX2.Y+HITBOX2.H)

IF LEFT < RIGHT AND TOP < BOTTOM THEN
RETURN TRUE
END

RETURN FALSE
END

This function determines whether a collision has occurred based on whether the hitboxes of two objects overlap. Where specifically they overlap is unimportant. The functions max() and min() are two other built-in math functions. Given two numbers, max() returns the higher one, and min() returns the lower one. The values x1 and y1 are the x and y-positions of one object, hitbox1 is that object’s hitbox, and and h are that hitbox’s width and height. Similarly, x2 and y2 are the x and y-positions of a second object, and hitbox2 is its hitbox. The collide() function returns true if a portion of one hitbox is contained within the other. If this is false, then there is a gap between the hitboxes, and there is no collision.

Sprite Addition and Player State Modification

Before I could call this function, I needed to add another sprite into the game. Referring back to the list of definite mechanics, the obvious choice was the trident. I created two functions, make_trident() and draw_trident():

FUNCTION MAKE_TRIDENT()
T={}
T.X=80
T.Y=80
T.HITBOX={X=4,Y=0,W=9,H=16}
T.SPRITE=NIL
END

FUNCTION DRAW_TRIDENT()
T.SPRITE=SPR(5,T.X,T.Y,2,2)
END

The trident was now displayed on the screen (40 pixels to the right of Satan), but it couldn’t be interacted with because I still hadn’t called collide(). In make_player(), I added the following value to the player table:

P.HAS_TRIDENT=FALSE

Since Satan starts without the trident, he doesn’t have it, and so p.has_trident is false when the game starts. I wanted it to be true when the two sprites collided. This would necessarily change Satan’s idle and profile sprites to the sprites carrying the trident, and the trident would need to disappear. To do all of this, I needed to write another function and slightly modify both draw_trident() (to only draw the trident if Satan didn’t have it) and draw_player() (to include the additional sprites):

FUNCTION UPDATE_PLAYER()

IF COLLIDE(P.X,P.Y,P.HITBOX,T.X,T.Y,T.HITBOX) THEN
P.HAS_TRIDENT=TRUE
END

END

FUNCTION DRAW_TRIDENT()

--DRAWS IF P.HAS_TRIDENT IS FALSE
IF NOT P.HAS_TRIDENT THEN
T.SPRITE=SPR(5,T.X,T.Y,2,2)
END

END

FUNCTION DRAW_PLAYER()

IF (P.HAS_TRIDENT==TRUE) THEN

IF P.IDLE THEN
P.SPRITE=SPR(33,P.X,P.Y,2,2)
ELSEIF P.LEFT THEN
P.SPRITE=SPR(35,P.X,P.Y,2,2,TRUE)
ELSEIF P.RIGHT THEN
P.SPRITE=SPR(35,P.X,P.Y,2,2)
END

ELSE

IF P.IDLE THEN
P.SPRITE=SPR(1,P.X,P.Y,2,2)
ELSEIF P.LEFT THEN
P.SPRITE=SPR(3,P.X,P.Y,2,2,TRUE)
ELSEIF P.RIGHT THEN
P.SPRITE=SPR(3,P.X,P.Y,2,2)
END

END

END

With that done, I placed a call to make_trident() in _init(), a call to update_player() in _update(), and a call to draw_trident() in _draw(). It took more time than I expected, but I couldn’t have been happier with the result:

cakequest_1