For project 1, you’ll be writing a video game in MIPS assembly. It’s a bit of a pastiche of a few games - a classic game called Boulderdash, a little Minecraft, a little of my own ideas.
You’ll continue to use the LED Keypad and Display plugin that you used in lab 4. To the right is a video demonstrating how the game will look and work when you’re done.
Brief game description
There is a tilemap on which all the action takes place. This is a 2D grid of “tiles,” small square images which make up the walls, vines, dirt etc.
The user controls the player, a yellow blob with a pointy blue hat. The player can move in all four directions, but not diagonally. The player can walk on some tiles, but cannot move through others.
There are dirt tiles which the player cannot move through, but can “dig up”, after which the player gets the dirt in their inventory. They can then place dirt tiles elsewhere.
There are diamonds which the player can collect for fun. There are vines which push the player back and prevent them from progressing. There are boulders which the player can push from the sides and will fall down the screen, but also function as obstructions.
There are bugs which are not enemies, but friends! Bugs automatically follow along walls and boulders, and can eat vines. Bugs cannot move boulders. So the player has to move boulders to help the bugs, and the bugs can eat vines to help the player. (They’re supposed to look like ladybugs, not lanternflies. Lol.)
The player’s objective is to rescue all the bugs by guiding them into the goal, a checkerboard-colored tile. Once all bugs have been rescued, the player can step onto the goal themselves to win the game.
Grading Rubric
Note: if you submit on the late due date, 10 points will be deducted after all other grading has been done.
Also note: if you submit the wrong thing (submit a file that isn’t your project, or an outdated version of your project) and it isn’t found until the grader looks at it, you will be allowed to resubmit, but you will lose 20 points. Be careful about what you are submitting. (But if you make a mistake in what you submit before the deadline, it’s no problem. Just resubmit with the correct file. You have infinite resubmissions before the deadline.)
Code style: although there is no point category for code style, the grader may take off up to 10 points if your code is very poorly-written. Poor indentation may lose you a couple points, but mostly it’s about using the calling convention we learned; using the right registers for the right purposes; and writing/using functions correctly. Remember: keep your code neat and tidy while you write it, not at the end.
- [46 points] Player
- [6] - Player is drawn (
obj_draw_player
) - [6] - Player can move in all 4 directions at correct speed and with proper delay
- [6] - Player cannot move through solid tiles
- [4] - “Camera” follows player around and tries to center them onscreen
- [6] - Player can dig up dirt tiles
- [6] - Player can place dirt tiles that have been dug up
- [6] - Stepping onto a vine causes player to step backwards
- [6] - Game ends when player reaches goal and all bugs have been saved
- [6] - Player is drawn (
- [10 points] Diamonds
- [4] - Diamonds are drawn (
obj_draw_diamond
) - [6] - Diamonds fall and can be collected (
obj_update_diamond
)
- [4] - Diamonds are drawn (
- [20 points] Boulders
- [4] - Boulders are drawn (
obj_draw_boulder
) - [4] - Boulders are updated properly (
obj_update_boulder
) - [4] - Player can push boulders to the left and right
- [4] - Player cannot push boulders if there is an obstruction past them
- [4] - Player cannot move through boulders from top or bottom
- [4] - Boulders are drawn (
- [24 points] Bugs
- [6] - Bugs are drawn (
obj_draw_bug
) - [10] - Bugs properly follow the walls/boulders on their left
- [4] - Bugs disappear when they step on the goal and count towards the “saved bugs” total
- [4] - Bugs eat any vine tiles they step on
- [6] - Bugs are drawn (
Stuff to download
Right-click and download this ZIP file. Your browser may automatically extract it to a folder. If not, open it and extract its contents to a new folder.
Now:
- Rename
abc123_proj1.asm
to your username. - Open that file in MARS.
- Open and connect the Keypad and LED Display Simulator tool.
- Assemble and run. It should just sit there in an infinite loop displaying some stuff. Hit the stop button to stop it.
How to approach this
Don’t be intimidated! You are only going to be writing around 500 lines of code (possibly more, if it’s heavily commented) to finish what I’ve given you in abc123_proj1.asm
.
Please follow the instructions in order. Later steps assume you have done earlier steps. Ten broken things are not as good as five fully-working things. You learn more from getting things working.
These instructions start verbose, but become more abstract as you go on. It’s because I have to introduce a bunch of concepts all at the beginning, and then as you get more comfortable with them, I can give you more general instructions and you can go “oh, I did something like this before.”
Also, this is a pretty sizeable project, and you’ve been given almost 3 weeks to do it. I would recommend you pace yourself and try to reach certain milestones each week. For example:
- by Thursday of week 1: parts 1, 2, and 3 (get everything drawn, move the player, and move the camera)
- by Thursday of week 2: parts 4, 5, 6, and 7 (dirt, diamonds, vines, and boulders)
- by Thursday of week 3: parts 8 and 9 (the goal tile, and bugs… you will need time for the bugs.)
What you’ve been given
You will only submit your abc123_proj1.asm
file. Do not put any code in any other file. You will not need to change any other file. (But you may change levels.asm
for testing as explained below).
abc123_proj1.asm
contains all the variables you’ll need, plus a number of functions.- Read the comments.
- Some functions are already filled in, like
check_game_over
; you won’t have to change those.
collide.asm
contains collision-related functions (for things bumping into other things).constants.asm
has the color and key constants for interacting with the display.display_2227_0611.asm
is a library of functions to interact with the display and keypad.- this is the full library that you only got a part of for the lab.
- you won’t use most of this, so don’t worry about it being so big 😅
game_constants.asm
contains constants specific to this game.levels.asm
contains data for a number of test levels for testing various features of the game.- You may e.g. change
level_1
or add new test levels for testing your code. However, we will be testing your game with the unmodified test levels that are given to you. - (If you make your own custom level that you want to show off, then it has to appear in your
proj1.asm
file so it gets turned in.)
- You may e.g. change
macros.asm
contains some very useful macros, which are like custom pseudo-instructions.- Each macro has a comment documenting what it does. You’re allowed to use any of them!
- I don’t recommend you make your own macros, many people have gotten confused that way.
map.asm
contains the code for loading levels fromlevels.asm
.obj.asm
is a library of functions for creating and manipulating game objects.textures.asm
contains the graphics for the game.tilemap.asm
contains the implementation of a large scrollable software-rendered tilemap which is used to draw the background of the game. There are a few functions you’ll use here as well.
What are enter
and leave
?
All the functions I’ve given you look something like this:
update_all:
enter
jal obj_update_all
jal update_timers
jal update_camera
leave
What are enter
and leave
? They’re macros I provided in macros.asm
:
enter
doespush ra
leave
doespop ra
and thenjr ra
So they act as the “braces” around functions. They replace the push ra
and pop ra
, jr ra
that you had to write by hand in the labs.
As an extra feature, if you want to use any s
registers, you list them in the same order after enter
and leave
:
my_function:
enter s0, s1 # same as pushing ra, s0, and s1
# in this function, I'm allowed to use s0 and s1.
# IMPORTANT: always list the same registers in the SAME ORDER as the 'enter'!
leave s0, s1 # same as popping s1, s0, and ra, then 'jr ra'
Only use enter
and leave
ONCE in each function. If you need to save multiple s
registers, list them as shown above. Do not do multiple enter
s or leave
s in a row. This will imbalance the stack and cause crashes!
These macros greatly reduce the amount of code you have to write for function prologues/epilogues. So now you really have no excuse for not using s
registers ;)
Seeing what the program does now
When you first run the program, this is what you’ll see.
Along the top is the HUD (“heads-up display”), which shows some important information about the game:
- 🔷
0
shows how many diamonds the player has collected. - 🟫
0
shows how many dirt tiles the player has dug up and is holding. - 🐞
0/5
shows that there are 5 bugs in the level, and 0 have been saved.
If you’re curious, look in map.asm
, at the contents of the level_data
array at the top. You can see that each character corresponds to one tile.
What’s all the stuff below the HUD? That’s the tilemap. It forms the background of our game, and is what the player will walk around on and interact with. Right now we can see some brick walls, some dirt (the brown speckled squares), and some vines. We are only looking at a tiny piece of the tilemap right now. Once the player can move around, we will see more of it.
Wait, where is the player? Well, let’s take a detour first…
Objects
The tilemap is the “stage” on which the objects appear and move around. Have a look at around line 30 of your proj1.asm
file. You’ll see these arrays:
# Object arrays. These are parallel arrays. The player object is in slot 0,
# so the "player_x" and "player_y" labels are pointing to the same place as
# slot 0 of those arrays. Same thing for the other arrays.
object_type: .word OBJ_EMPTY:NUM_OBJECTS
player_x:
object_x: .word 0:NUM_OBJECTS # fixed 24.8 - X position
player_y:
object_y: .word 0:NUM_OBJECTS # fixed 24.8 - Y position
player_vx:
object_vx: .word 0:NUM_OBJECTS # fixed 24.8 - X velocity
player_vy:
object_vy: .word 0:NUM_OBJECTS # fixed 24.8 - Y velocity
player_moving:
object_moving: .word 0:NUM_OBJECTS # 0 = still, nonzero = moving for this many frames
player_dir:
object_dir: .word 0:NUM_OBJECTS # direction object is facing
Just like the particles in lab 4, these are parallel arrays. For example, the object in “slot 4” has a type in object_type[4]
; a position in object_x[4]
and object_y[4]
; a velocity vector in object_vx[4]
and object_vy[4]
; and so on. Each of these arrays is NUM_OBJECTS
long.
The types of objects are the OBJ_
constants in game_constants.asm
. OBJ_EMPTY
is a special value to mean “no object in this slot.” The object_type
array is initially filled with OBJ_EMPTY
but objects are created when the map is loaded by load_map
(called from main
).
The object in slot 0 is special: it is always the player object. That’s why the player_x
, player_y
, player_vx
, player_vy
etc. labels are pointing at the beginnings of those arrays. (If you have multiple labels on one .word
, all the labels point to the same place.)
How objects are shaped
Each object is a square centered on its position in the object_x
and object_y
arrays. It is OBJ_SIZE
pixels wide and tall, and the distance from the center to any side is OBJ_HALF_SIZE
. (See diagram to the right; the dot in the middle is the position.)
If you look at the definitions of these constants in game_constants.asm
, you’ll see:
.eqv OBJ_SIZE 0x500
.eqv OBJ_HALF_SIZE 0x280
0x500
? 0x280
? That’s because these are fixed-point integers. We are using 24.8 fixed-point integers in this project: 24 bits of “whole number” followed by 8 bits of fraction.
If you write these numbers in hex, it works out that the last two hex digits are the fraction, and the digits before them are the whole part. So 0x500
means “5.0”, and 0x280
means “2.5” (because the fraction is 0x80 / 0x100 = 128 / 256 = 0.5
). You’ll get used to it :)
Object methods
MIPS has no built-in classes, but that doesn’t stop us from doing object-oriented programming. Each type of object in the game has two methods:
obj_update_TYPE
“updates” that type of object (makes it do whatever it is that it does).obj_draw_TYPE
draws that type of object to the screen.
For example, the player object has obj_update_player
and obj_draw_player
, both in your proj1.asm
. Both methods take the object index as the a0
argument, so within these methods, you can use a0
as the indexes into the various object_
arrays. (Essentially a0
is the this
argument.)
1. Drawing the objects
It’s hard to get any work done if you can’t even see the things you are trying to implement! So let’s draw all the objects by implementing the obj_draw_
methods.
1.1 Drawing the player
All the objects in this level actually already exist! They are not visible because all the obj_draw
methods in proj1.asm
are empty. So let’s implement obj_draw_player
.
- When
obj_draw_player
runs, the object index is already ina0
. So we can immediately callobj_get_topleft_pixel_coords
.- It takes an object index in
a0
, and places the pixel coordinates of the top-left corner of the passed-in object into the return value registers.
- It takes an object index in
- Move those coordinates into the first two argument registers, because we’re about to call another function.
- The third argument will be
player_textures[player_dir * 4]
.player_dir
is what kind of variable? so how do you get its value?- then load
player_textures[player_dir * 4]
intoa2
.- this is a
.word
array.
- this is a
- Finally, call
blit_5x5_sprite_trans
.- This is a function at the bottom of
proj1.asm
that adjusts the X and Y coordinates based on the position of the “camera” (which we’ll get to soon).
- This is a function at the bottom of
You should now see the little yellow blob guy on screen when you run the program, like in the screenshot above!
1.2. Drawing the bugs
There are some bugs, too. Drawing them is pretty similar to drawing the player, so let’s get that out of the way.
First, change obj_draw_bug
like so:
obj_draw_bug:
enter s0
move s0, a0
leave s0
Doing enter s0
and leave s0
gives us permission to use s0
in this function, and the move
puts the object index into s0
. This is good practice to use for the object methods moving forward. (We were able to get away with not doing this in obj_draw_player
because we have access to the player_
variables.)
Now, the rest of obj_draw_bug
is almost the same as obj_draw_player
:
- call
obj_get_topleft_pixel_coords
and move the return values into the argument regs - index an array of textures, but this time it’s
bug_textures[object_dir[s0]]
- yes, that’s an array index inside an array index… what happens first?
- you do NOT need to multiply
s0
by anything, it is already pre-multiplied - loading out of
object_dir[s0]
can be done in a single line! - ..but you DO need to multiply the value that you load out of
object_dir[s0]
, becausebug_textures
is a.word
array!
- call
blit_5x5_sprite_trans
Boom, there are the bugs!
1.3 Drawing the diamonds and boulders
These are even easier than the player and bugs.
For each of obj_draw_diamond
and obj_draw_boulder
:
- call
obj_get_topleft_pixel_coords
and move the return values into the argument regs - use
la
to put a texture address intoa2
tex_diamond
for diamonds,tex_boulder
for boulders
- call
blit_5x5_sprite_trans
I guess you don’t really need to use s0
for these… But there you go.
You now have a 20%!
2. Moving the player around
Now we need to move the player. This will have some similarities to lab 4, but we’re also going to create a good amount of “code scaffolding” to support it as well as a bunch of future steps of the project.
obj_update_player
Here is my sketch for the code that goes in obj_update_player
. Translate this code, and stub out each of the 5 player_
functions that it calls.
- You can stub out a function by writing
enter
andleave
with nothing between them, like the other empty functions. - the 0 in
obj_move(0)
is important!obj_move()
expects the object index as its first argument; index 0 is the player object, so we’re telling it to move the player. - make
player_check_vines
just return 0 for now.
{
// if player is not moving...
if(player_moving == 0) {
// see if the player is on a goal tile
player_check_goal();
// see if the player is on a vine tile, and bail if they are
if(player_check_vines() == 0) {
// see if they want to place dirt
player_check_place_input();
// and see if they want to move
player_check_move_input();
}
} else {
// they're moving, so move them!
obj_move(0);
}
// always check for dig inputs
player_check_dig_input();
}
player_check_move_input
This function checks to see if the player wants to move with the arrow keys. This is kind of similar to what you did on lab 4, but simpler:
- call
input_get_keys_held
- put its return value in
s0
(what do you have to do to be allowed to use it?) - check for them pressing the four directions, just like you did on lab 4, except:
- the keys being held are in
s0
instead ofv0
- in each case, you will be calling
player_try_move
, another new function, with an argument KEY_U
should callplayer_try_move(DIR_N)
KEY_D
should callplayer_try_move(DIR_S)
KEY_L
should callplayer_try_move(DIR_W)
KEY_R
should callplayer_try_move(DIR_E)
- remember to use e.g.
li a0, DIR_N
, don’t copy and paste the constant value! use the name!
- remember to use e.g.
- the keys being held are in
player_try_move
Make this function, and at first, just paste this code into it (“into” means between the enter
and leave
so don’t forget those)
print_str "trying to move "
syscall_print_int
newline
Now assemble and run, click the display, and hit the arrow keys. You should see some messages being printed:
- hitting the up arrow should show “trying to move 0”
- the right arrow should show “trying to move 1”
- the down arrow should show “trying to move 2”
- and the left arrow should show “trying to move 3”
If you don’t see anything printed, here are some common mistakes:
- did you make
player_check_vines
return 0? - is the control flow in
obj_update_player
correct? (does it match the Java pseudocode I gave?) - are you sure that
player_check_move_input
is actually running? - in
player_check_move_input
, are you looking at the return VVVVVVVVVVVVVvvvalue frominput_get_keys_held
?- it works just like what you did on lab 4…
If it continuously prints messages even when you don’t hold any arrow keys, your if
s in player_check_move_input
are probably using the opposite branch type than they should. You want to skip each if’s code when the and
gives you 0.
If you still can’t get it printing properly, you know what you should do by now. (The answer is not “sit there for two days wondering what you did wrong and feeling bad about it”)
Alright, now delete those 3 lines of printing code. player_try_move
’s responsibility is to, well, try to move the player. The player can’t always move, for example if they’re trying to move into a wall. So we have to do some checks first to verify that they can move, and only then start them moving. Here’s the sketch for this function:
// are they changing directions?
if(player_dir != a0) {
player_dir = a0;
// prevent them from moving immediately so that
// they can turn without moving
player_move_timer = PLAYER_MOVE_DELAY;
}
// only allow them to move every PLAYER_MOVE_DELAY frames
if(player_move_timer == 0) {
player_move_timer = PLAYER_MOVE_DELAY;
switch(obj_collision_check(0, player_dir)) {
case COLLISION_TILE: return;
case COLLISION_OBJ:
// TODO: object pushing, but later.
// fall through to move case (do NOT break out of the case here)
default: // "move" case
obj_start_moving_forward(0, PLAYER_MOVE_VELOCITY, PLAYER_MOVE_DURATION);
}
}
To explain broadly:
- the first
if
allows the player to change directions without moving, which is a nice quality-of-life feature. - the second
if
only allows the player to move once every certain amount of time. obj_collision_check
checks for collision in front of an object.- the
switch
is looking at the return value (v0
) ofobj_collision_check
- we’ll fill in the
COLLISION_OBJ
case later, but for now we can leave it empty. totally empty. noj _break
even. obj_start_moving_forward
does exactly what it sounds like it does!
- we’ll fill in the
Now, if you assemble, run, click on the display, and use the arrow keys, you should be able to change what direction the player is facing. But they can’t move yet. Why?
It’s because player_move_timer
is never being updated. We set it to PLAYER_MOVE_DELAY
but never decrement it so it’s never 0 so the second if
never runs.
To fix that, find the update_timers
function. In it, decrement player_move_timer
, but only if it’s greater than 0. (It should never go negative, is what I mean.)
Now you should be able to move the player around! They should stop at the brick walls and dirt, and pass through other things like vines, boulders, diamonds, and bugs.
If you press an arrow key once and the player quickly moves offscreen without you holding it down: you messed up the if-else
in obj_update_player
and you forgot to jump over the else part. (am I psychic?)
And if you go off the right side of the screen… lol bye. The player disappears offscreen and we can’t get them back. We have to make the camera follow the player now!
Tangent: what’s obj_collision_check
doing?
Uh, quite a bit actually. The source code is in obj.asm
, which also calls a function from collide.asm
. It basically checks to see if there is a solid tile or solid object one “tile” away from the center of the object in the given direction.
If there is nothing solid there, it returns COLLISION_NONE
which means we can move freely. If there is a solid tile, it returns COLLISION_TILE
which causes the switch
in player_try_move
to return, disallowing the player from moving. If there is a solid object, it returns COLLISION_OBJ
which we will handle later.
3. Moving the camera (this is a short one)
Remember I said we were only looking at a portion of the tilemap? The “camera” is really the position of that portion that we are currently looking at. “Moving the camera” amounts to moving the tilemap around.
Find update_camera
. In there, you need to:
- Get the top-left pixel coordinates of the player (object 0)
- you called the function for this in all your
obj_draw
methods, but… - this function takes an argument. in the
obj_draw
methods, that argument was already ina0
, but now you have to put it there yourself. The player is object zero, so put 0 ina0
before calling this function.
- you called the function for this in all your
- Add
CAMERA_OFFSET_X
andCAMERA_OFFSET_Y
to the return values of that function, and put the results of the additions into the argument registers - Call
tilemap_set_scroll
That’s it. Now when you move the player down or to the right, the camera follows!
You now have a 36%!
4. Digging and placing dirt
The player can now move around, but that alone is a little boring. Let’s let them dig up dirt and then place it elsewhere.
For that, it might be useful to use one of the test levels I’ve made to test specific features. Look up at the start of main
:
main:
# load the map and objects
la a0, level_1
#la a0, test_level_dirt
#la a0, test_level_diamonds
#la a0, test_level_vines
#la a0, test_level_boulders
#la a0, test_level_goal
#la a0, test_level_bug_movement
#la a0, test_level_bug_vines
#la a0, test_level_bug_goal
#la a0, test_level_blank
jal load_map
See all those commented-out la
lines? Each one of those is a test level for testing those specific features. Right now, we’re telling load_map
to load level_1
, but if we comment out that line and uncomment the test_level_dirt
line like this:
#la a0, level_1
la a0, test_level_dirt
#la a0, test_level_diamonds
#la a0, test_level_vines
#la a0, test_level_boulders
#la a0, test_level_goal
#la a0, test_level_bug_movement
#la a0, test_level_bug_vines
#la a0, test_level_bug_goal
#la a0, test_level_blank
jal load_map
When we run the game now, we see a totally different layout of tiles designed for testing the dirt-digging and placing abilities we are about to implement. Be sure to switch between test levels as you implement things. For grading, we will grade using level_1
, and possibly use the other test levels to narrow down what works and what doesn’t.
4.1 Digging dirt
The player should be able to dig up dirt tiles on the tilemap by facing a dirt tile and pressing X.
In player_check_dig_input
:
- Call
input_get_keys_pressed
- this is a new function different from
input_get_keys_held
, and returns a bitflag value of all the keys that went from “not pressed” to “pressed” on this frame. - it’s useful for actions that you don’t want to repeat when the key is held down.
- this is a new function different from
- Check the return value for
KEY_X
just like you would withinput_get_keys_held
. If they pressed X…- Get the tile coordinates in front of the player object.
- Open this page in another tab and keep it around. Find an
obj_
method that gives you what you need.
- Open this page in another tab and keep it around. Find an
- Get the tile at those coordinates.
- If it’s dirt (
TILE_DIRT
),- Set the tile at those coordinates to
TILE_EMPTY
- Increment
player_dirt
, but only if it’s less thanPLAYER_MAX_DIRT
- Set the tile at those coordinates to
- Get the tile coordinates in front of the player object.
This isn’t very complicated code, but I’m hoping you can be a little independent about looking up the functions you need in the reference page! This is a realistic simulation of what a lot of “real” programming is like: you have a library of functions and documentation for them, and it’s up to you to “glue them together” to solve a problem.
Done correctly, you should now be able to walk up to the dirt tiles, press X, and the tile should disappear and the count of dirt at the top of the screen should increase.
Be sure to test:
- That you can dig dirt in all four directions
- That if you hold X and move, you do not dig continuously
- you should have to repeatedly tap X to dig each dirt tile
- That if you dig up all the dirt in the test level, you have 🟫
20
at the top
Also you can test the “maximum dirt” by temporarily changing player_dirt
to 95, then digging some dirt; your dirt counter should never go above 99.
4.2 Placing dirt
Placing dirt is similar to digging, but a little more complicated because we have to do some more checks to see if it’s okay. The player should be able to place dirt tiles on the tilemap by facing an empty tile and pressing Z when they have at least 1 dirt tile in their inventory.
There is an additional condition here. At the very top of your proj1.asm
, see this?
.eqv GRADER_MODE 0
If this is 1, it will allow you to place dirt even if you haven’t dug any up yet.
Anyway. Here’s how player_check_place_input
will work:
- If they pressed the Z key (just like in
player_check_dig_input
)- if
GRADER_MODE
is not 0, OR ifplayer_dirt > 0
…- and the tile in front of them is empty…
- and there is no object at the pixel coordinates in front of them… (see below)
- THEN you can set that tile to
TILE_DIRT
and decrementplayer_dirt
.
- THEN you can set that tile to
- and there is no object at the pixel coordinates in front of them… (see below)
- and the tile in front of them is empty…
- if
Wow that’s a lot of “and”s. Here’s a tip on how to write a long nested conditional like this: put a _return:
label before the leave
, and just branch to _return
whenever you need to give up (whenever any condition is NOT satisfied). So you’ll branch to it if they AREN’T pressing Z, and when player_dirt
is 0, and when the tile in front of them is NOT empty etc.
“There is no object at the pixel coordinates in front of them” - you’ll need obj_get_pixel_coords_in_front
for the first part of this, and you should use TILE_SIZE
as the distance argument. From there, which function lets you see if an object exists at those coordinates?
You should now be able to dig up dirt with X, then place it elsewhere with Z.
Be sure to test:
- That you can place dirt in all four directions
- That if you have 0 dirt, that you cannot place dirt
- …unless you change
GRADER_MODE
to 1, in which case you will be able to place dirt even with 0 dirt!
- …unless you change
- That you cannot place dirt on brick or dirt tiles
- That you cannot place dirt on the diamond or boulder objects on the left
You now have a 48%! You’re about halfway done!
5. Diamond objects
Let’s do something easy: diamond objects. (Use the test_level_diamonds
level for this.)
Diamond objects behave very simply:
- they sit there.
- …unless there is nothing below them, in which case they fall down.
- if the player touches them, they disappear and the player gets a diamond.
So here’s what obj_update_diamond
needs to do:
- put the object index into
s0
! - call
obj_move_or_check_for_falling
- this does the work of seeing if the diamond should fall, and if it should, it moves it.
- see if the diamond object is colliding with the player. there’s an
obj_
function for this.- don’t forget you have to pass the object index as the argument!
- if it is,
- increment
player_diamonds
- free the diamond object (pass that argument!)
- increment
As shown in the animation to the right, be sure to test that:
- you can collect diamonds and the count of diamonds increases
- if you dig the dirt out from under a diamond, it falls down
- even a stack of diamonds will fall down, and if you move under the stack, they fall onto you and you collect them :)
- isn’t that satisfying?
6. Vine tiles
Use test_level_vines
for this. If you try it right now, you can walk all over the vine tiles like they’re not even there. That’s wrong.
The vines are supposed to be an obstacle. They’re uh, all thorny and stuff. So you can step on them, but it hurts, and so you step back immediately. Yeah.
A while ago I had you write some code in obj_update_player
. It called player_check_vines
and if it returned 1, it skipped getting input from the player. Now we’ll implement player_check_vines
.
Remove the code in player_check_vines
that makes it always return 0. Instead it should:
- See if the tile that the player is standing on is a vine tile (
TILE_VINES
) - If so,
- use
obj_start_moving_backward
on the player to move them backwards using the same velocity and duration as you did withobj_start_moving_forward
- return 1
- use
- else (NOT standing on a vine tile),
- return 0.
That’s it. Test that:
- If you step on a vine tile from all four directions, that it pushes you back
- That you cannot move through a vine tile in any way
- But if you dig the dirt out from under the diamond, the diamond falls through the vine tile.
You now have a 60%!
7. Boulders
Use test_level_boulders
for this.
Boulders are mostly kind of dumb objects that don’t do much, but the code to allow the player to push them from the left or right can be tricky.
First let’s get obj_update_boulder
out of the way: just call obj_move_or_check_for_falling
. That’s it. Seriously. One line. If you test it now, you should be able to dig dirt out from under the boulders and watch them fall, just like the diamonds did. See, common behavior implemented in one reusable function!
Now let’s revisit player_try_move
. Remember that case for COLLISION_OBJ
that was marked as “TODO” before? Let’s fill that out:
- Pass
v1
as the argument to a new functionplayer_try_push_object
- If
player_try_push_object
returned 0, return fromplayer_try_move
. Otherwise, fall into the “move” case (where you callobj_start_moving_forward
)
Why v1
? Becuase if obj_collision_check
returned COLLISION_OBJ
in v0
, then v1
contains the index of the object that is in the way! So we are going to try to push that object with player_try_push_object
.
player_try_push_object
takes the object to try to push in a0
and returns a boolean: it returns 1 if the player should be able to move, and 0 if the player should not be able to move (if they are pushing a boulder from the wrong direction).
Tangent: bending the rules of control flow structures
There are basically three outcomes of this function:
- Push the object and return 1 to say “yes, move”
- Return 1 (without pushing the object)
- Return 0 to say “no, don’t move”
Rather than trying to massage this logic into a complicated set of nesting ifs, it’s honestly easier to treat it like a weird kind of switch-case, where most of the logic is in the switch, and the three cases are tiny. So you can structure this function like:
player_try_push_object:
enter
# a bunch of code, where some branches branch to _push,
# _return_yes, or _return_no
_push:
# push the object!
# fall into the next case
_return_yes:
li v0, 1
j _return
_return_no:
li v0, 0
_return:
leave
Alright. Here are the rules for pushing:
- The player can only push boulder objects. Any other type of object, the player can move into.
- There is no method to get an object’s type, because you don’t need it - just index the
object_type
array with the object’s index that was passed into this function.
- There is no method to get an object’s type, because you don’t need it - just index the
- The player can only push a boulder if they are facing east or west. Other directions, the player cannot move.
- The boulder object cannot be moving. (When its
object_moving
is nonzero, it is moving.) If it is, the player cannot move. - If there is a solid tile or boulder on the other side of the boulder being pushed, the player cannot move.
- This is a tricky one, but it’s just using functions you’ve already called.
- Hint: “the other side” is in the same direction that the player is trying to push…
- If you got through all those conditions,
- push the object in the direction the player is facing! there’s an
obj_
function for this. - and return 1 to say the player can move.
- push the object in the direction the player is facing! there’s an
Things to test:
- you can push boulders left or right
- you can push boulders in the middle of a stack
- pushing down on a boulder from above does nothing (player does not move)
- pushing a boulder into another boulder does nothing, in both directions
- pushing a boulder into a wall does nothing, in both directions
Wow, that was a big chunk of points! you now have a 76%.
8. The goal tile
Use test_level_goal
for this.
The goal tile is how you win the game, and its behavior is pretty simple. Here’s how player_check_goal
works: if the player is standing on a goal tile, AND bugs_saved == bugs_to_save
, then set game_over = 1
.
That’s it. That’s all you do. Do not jal show_game_over_message
or do syscall 10 or anything like that. All that control flow is handled by main
and check_game_over
. All you have to do is set the game_over
variable to 1.
When you step on the checkered goal tile, you should now see a congratulation message along with a display of how many diamonds you collected!
You’re at an 82%.
9. Bugs, the final boss of the project
The bugs are your friends. They navigate through the level on their own and eat vines that they step on. Your goal is to get them to the goal; once all the bugs make it to the goal, you can go into the goal too.
The overall shape of obj_update_bug
is like:
// assuming you moved the object index argument into s0!
if(object_moving[s0] != 0) {
obj_move(s0);
} else {
if(bug is not on a goal tile) { // part 9.3
eat vines; // part 9.2
move; // part 9.1
}
}
It’s up to you to split this code up into as many functions as you like. You could do everything in object_update_bug
but it would be HUGE and confusing.
9.1 Bug movement
Use test_level_bug_movement
for this.
This is definitely the hardest part of the project. After this, the other parts are easy. Bug movement works like so:
- If there is something solid in front of the bug…
- note that down but don’t do anything yet.
- If there is something solid to the left of the bug…
- if there was something solid in front of the bug in step 1,
- turn the bug to the right.
- else,
- move the bug forward with
BUG_MOVE_VELOCITY
andBUG_MOVE_DURATION
- move the bug forward with
- if there was something solid in front of the bug in step 1,
- Else (there was nothing solid to the left of the bug)…
- turn the bug to the left,
- then move the bug forward with
BUG_MOVE_VELOCITY
andBUG_MOVE_DURATION
Yeah, it’s complicated. But it’s doable. “Is there something solid” and “move the bug forward” are things you know how to do.
“To the left of” and “turn the bug” are actually really easy to implement if you’re clever. Notice how the directions are defined:
# Cardinal directions.
.eqv DIR_N 0
.eqv DIR_E 1
.eqv DIR_S 2
.eqv DIR_W 3
The directions increase clockwise (turning to the right). They are in the range [0, 3], which is conveniently also the range of integers you get if you perform modulo by 4… And you know two ways to do that!
Good luck. Implemented correctly, the bug should walk around the entire perimeter of the test room as shown in the animation to the right. Also if you move down you can see three more bugs used to test some cases of movement. Yours should behave the same. (Also, poor bug in the bottom-left…)
If the bug walks upwards and then the game crashes with an alignment error as soon as it reaches the top wall, it’s very likely a bug (lol) in your obj_draw_bug
function. Cmon, you know what alignment errors on array loads mean by now. I hope. 😬
You’re at a 92%.
9.2 Bugs eating vines
You’re in the home stretch now. Use test_level_bug_vines
.
Bugs eating vines should now be a piece of cake for you. If a bug is sitting on a vine tile, set it to an empty tile. That’s all!
9.3 Bugs reaching the goal
Last piece. Use test_level_bug_goal
.
Again this should be trivial. If a bug is sitting on a goal tile, free it, increment bugs_saved
, and make sure the rest of obj_update_bug
DOES NOT RUN because the bug is gone now.
Importantly, if you touch the goal before the bug gets to it, the game should not end! If it does, go back to player_check_goal
because something is wrong there. (The animation shows this.)
You’re done!
Try the level_1
level and see if you can beat it. You have to help the bugs and they will help you. Placing dirt can help corral the bugs into the directions you want them to go. See if you can get all 18 diamonds too.
Submission
Be sure to review the grading rubric before submitting.
You will submit only your abc123_proj1.asm
file (but renamed with your username).
Please put any important notes to the grader at the top of your asm
file in comments. For example, if you wrote some code that is never called, they will not see the behavior; tell them that you attempted it and you may get some partial credit.
To submit:
- On Canvas, go to “Assignments” and click this project.
- Click “Start Assignment.”
- Under “File Upload,” click the “Browse” button and choose your
abc123_proj1.asm
file.- Do not submit any other files.
- Click “Submit Assignment.”
If you need to resubmit, that’s fine, just click “New Attempt” on the assignment page and upload it again. The last thing you submit is what we grade. But remember, if the last thing you submit is the wrong thing, you will receive a 20 point penalty.
It is okay to submit on/before the normal due date, and then resubmit on the late due date. You will get the late penalty, but if you turn in a 60 on time and a 100 late, that 100 becomes a 90, and it’s a net win anyway!