This week’s lab is a departure from the usual “printing text on a console” that I’m sure everyone just loves. Instead, you’ll be making an interactive graphical program. Yes, really! See that animation on the right? That’s what you’re making.
What is this?
A particle system is a way to simulate collections of small objects called “particles” which all behave the same way. Each particle on its own is not too interesting, but put enough of them together, and you can get something really beautiful. In the animation to the right, it looks like we’re spraying droplets of water or something. Each droplet is a particle.
Particle systems are ubiquitous in computer graphics in video games, animation, visual effects for film and TV, and even graphical interfaces in apps and on websites. They’re used to simulate things like rain, snow, fog, smoke, fire, water, steam, clouds, swarms of insects, flocks of birds, and so much more. They’re also nice because they require only a small amount of code to make something that looks impressive. Perfect for a lab assignment!
How do you even make a program like this?
If you’ve never made a program like this before, it may look intimidating, but it’s really not. This is training for the first project, where you’ll make a small video game. This lab has all the same parts as a video game, but very simplified.
Real-time interactive programs - including video games, GUI (graphical user interface) apps, web browsers, and just about any other program you use on a daily basis - all work basically the same way:
- check for user inputs
- respond to those inputs by updating your program state (the variables, data structures, etc.)
- change the output (screen) to reflect the new state
- wait for a little while
- loop back to step 1
You’ve probably done simple games in programming classes before: get inputs from the user, update variables based on game rules, loop back to asking for more input. The difference now is… it’s faster. The player gets 60 turns every second! AAAAAH!
0. Getting started
Right-click and “save link” or “download link” both of these files into the same directory:
abc123_lab4.asm
- Rename this to have your username instead of
abc123
- Rename this to have your username instead of
lab4_include.asm
- You can look inside this file, but do not modify it - you will not turn it in.
Make sure they are really saved as .asm
files and not .asm.txt
files!
Now open up your lab4.asm
and let’s have a look.
What’s in lab4.asm
First it includes lab4_include.asm
. Our programs are getting bigger, and it’s not reasonable to put all the code in one file anymore. This is kind of like an import
in Java (but it’s more like a #include
in C, if you’re familiar with that). You can have a look at lab4_include.asm
too if you like!
Then we have a number of constants, the .eqv
lines. What these constants are used for will be explained as we need them.
Then there are the variables and arrays. Again, they’ll be explained as needed.
Then there is the main
function, which is written for you, though some lines inside the main loop are commented out. You’ll be uncommenting them later.
Finally there is the find_free_particle
function. This is just a helper for a function you’ll write later.
The display
In MARS, go to to Tools > Keypad and LED Display Simulator. Not Keyboard and Display MMIO Simulator. This will pop up a window. Click the “Connect to MIPS” button in the bottom left.
Once it’s connected, you don’t have to close the window or reconnect it. You can re-assemble and re-run your program as many times as you want while the display is open. I mean, yeah, it might be awkwardly in the way so you can close it, but… I’m just saying you don’t have to close it and reopen it every time you run your program.
How the display works
- The display is 64 pixels wide by 64 pixels tall.
- Each pixel can be one of 16 colors, numbered 0 through 15, but there are also named constants for these colors in
lab4_include.asm
.
- Each pixel can be one of 16 colors, numbered 0 through 15, but there are also named constants for these colors in
- The origin (0, 0) is at the top-left.
- The X axis increases to the right, and the Y axis increases down.
- You can see how this works in the image to the right.
- (This is typically how graphical displays are arranged and accessed, because they grew out of text displays, where this arrangement is natural, as text starts at the top left.)
- The pixel data is stored in a 2D array in memory. You won’t be accessing this array directly, but it means there are two steps to displaying stuff:
- Draw as many pixels as you want using the provided
display_set_pixel
function; then - Use the provided
display_update_and_clear
function to copy the data from memory to the screen.
- Draw as many pixels as you want using the provided
- You can think of it like drawing on a piece of paper, and then when you’re done with the drawing, you give it to someone and they stick it to the fridge for everyone to see.
display_set_pixel
is “drawing on the paper,” so you keep calling it to draw as many dots as you want.display_update_and_clear
is “sticking it on the fridge for everyone to see,” so you do it once after drawing everything.
1. Drawing the emitter
Try assembling and running your program. It should assemble and run just fine, but the display will just show a black screen. Boring. But this program runs as an infinite loop! So you need to hit the “stop” button in MARS to make it stop running.
The emitter is the point on the screen from which the particles will be created. If we’re going with a water analogy, the emitter is like the end of the hose where water comes out. The user will control the position of the emitter in this program by using the arrow keys on the keyboard.
In the .data
segment of your lab4.asm
file, you will see these two variables declared:
# position of the emitter (which the user has control over)
emitter_x: .word 32
emitter_y: .word 5
These are the X and Y coordinates of the emitter.
If we want to see the emitter, we need to draw it to the display. Do the following:
- Make a new function called
draw_emitter
. It will have no arguments and return nothing.- Remember what all functions must begin and end with?
- Uncomment the
jal draw_emitter
line in the loop inmain
.- At this point, you can assemble and run your code to make sure nothing has changed. (No news is good news.)
-
Inside
draw_emitter
, you’ll do this (please read the stuff after the code before you start):display_set_pixel(emitter_x, emitter_y, COLOR_WHITE);
display_set_pixel
is a function that is given to you in the include file. You don’t have to write it.- Don’t write
li a2, 7
. Use the names of constants.li a2, COLOR_WHITE
makes much more sense.
This is your goal. Done correctly, when you run your program you should now see this on your screen. If you don’t, something is wrong, and you should fix it before moving on.
Well that’s… exciting? A single white dot. Okay, it’s not that exciting. But now we can make it interactive!
How input works
If you click on the display while your program is running, you can use the arrow keys (up, down, left, right) and the Z, X, C, B keys to interact with it.
When you call the provided input_get_keys_held
function, it returns a value where each of those eight keys is represented by one bit, where 1 means the key is held down, and 0 means it isn’t. In otherwords, it returns a bitflag value.
The KEY_
constants near the top of lab4_include.asm
exist so you don’t have to remember which bit means which key.
2. Moving the emitter around
Moving the emitter is very simple: if we change the contents of the emitter_x
and emitter_y
variables, then it will be drawn in a different place on the screen. All we have to do is make it so the arrow keys change the values of these variables.
- Make a new function called
check_input
. It will have no arguments and return nothing. - Uncomment the
jal check_input
line in the loop inmain
. - In
check_input
:- call
input_get_keys_held
(another function from the include file) - then write some code that does this (again read the stuff after the code before attempting):
if((v0 & KEY_L) != 0) { if(emitter_x != EMITTER_X_MIN) { emitter_x -= 1; // same as "emitter_x--" } }
&
is doing a bitwise AND here; theand
instruction is what you want. (this is NOT a substitute for&&
.)- Remember from the bitflags part of lecture that this is “extracting” a single bit from the value in
v0
so that we can check if that bit is 0 or not.
- Remember from the bitflags part of lecture that this is “extracting” a single bit from the value in
- Do not put the result of the
and
back intov0
because we’re gonna be doing more checks on it after this code. Use your friendt0
as the destination. - You can share one “endif” label for both ifs. But name it like
_endif_l:
or something since we’re going to have more ifs in this function.- (this “nested if” is actually the same thing as
&&
if you think about it!)
- (this “nested if” is actually the same thing as
- call
Now test it:
- Run your program.
- The dot should sit still.
- Click on the display!! (very important)
- Hit the left arrow key on your keyboard, not the
L
key- You should see the dot move to the left, and then stop at the left side of the screen, as shown in the animation to the right.
If it doesn’t move at all: did you invert the inner if’s condition? How about the outer if? They should both be using beq
…
If it moves to the left, but doesn’t stop at the left side and goes offscreen: your inner if is probably malformed somehow. Like you branched to the code inside the if.
Something else? Get help.
Now we’ll make it work for all the directions. Repeat that if
three more times, changing it like so:
KEY_R
should incrementemitter_x
, as long as it’s notEMITTER_X_MAX
KEY_U
should decrementemitter_y
, as long as it’s notEMITTER_Y_MIN
- remember, on the display Y increases downwards, so up is negative
KEY_D
should incrementemitter_y
, as long as it’s notEMITTER_Y_MAX
Tips:
- as long as you never change
v0
, you can keep using it in eachand
. - when checking the result of the
and
, DO NOT change thebeq
conditions to check for anything other than 0! Anand
result of 0 means “not pressed” and nonzero means “pressed.” Don’t worry about what the nonzero value is. It doesn’t matter. - when checking the emitter coordinates, don’t forget that you can use constants inside conditions, and this makes your conditions much more readable. Like this is totally fine:
lw t0, emitter_x # if emitter_x is not EMITTER_X_MIN, go to the endif bne t0, EMITTER_X_MIN, _endif_l
- don’t forget to change the label name in both the
beq
s and at the end of eachif
! Otherwise you can end up with some really strange bugs.
Now it should behave like in the animation to the right:
- you can move the emitter in all four directions (up moves up, down moves down etc)
- you can move the emitter diagonally by holding two directions at a time (e.g. up-left or down-right)
- the emitter stops at all four sides of the screen and never goes offscreen
As usual, get help if you can’t get this working.
Tangent: how the particles are represented
In Java, we have classes to represent objects. You can give classes instance fields, and every time you new
, you get a new object with its own copies of these fields.
This isn’t really a great match for how assembly works. We’ll be using a different way of representing objects instead. Look up in the .data
segment. You’ll see these arrays being declared:
# parallel arrays of particle properties
particle_active: .byte 0:MAX_PARTICLES # "boolean" (0 or 1)
particle_x: .half 0:MAX_PARTICLES # unsigned
particle_y: .half 0:MAX_PARTICLES # unsigned
particle_vx: .half 0:MAX_PARTICLES # signed
particle_vy: .half 0:MAX_PARTICLES # signed
(The syntax 0:MAX_PARTICLES
means “make an array MAX_PARTICLES
items long, and fill it with 0s”. It would be like doing new short[MAX_PARTICLES]
or so in Java.)
All of the particle_
arrays are parallel arrays. They’re all the same length (MAX_PARTICLES
). An “object” is just the set of values with the same index. For example, particle 0’s active
field is particle_active[0]
; its position is particle_x[0]
and particle_y[0]
; and its velocity vector is particle_vx[0]
and particle_vy[0]
.
The particle_active
array is an array of “booleans” (bytes, but only ever the values 0 for false
or 1 for true
). All the particles start off inactive (0). When we need a new particle, we search this array for an inactive particle. Then we can mark it active (1), fill in its other properties, and the particle is now “alive.” Later, to “get rid of” a particle, we just set its particle_active
back to 0.
A downside to this system is that we have a fixed maximum number of particles available. We can never have more than MAX_PARTICLES
particles at one time. But the upside is it’s extremely simple to implement, and is actually very fast.
3.1. Spawning particles
Now things are going to get more interesting. What we want to happen is when the user presses the B
key, a new particle will “spawn” (be created) at the emitter’s position. Here’s how:
- At the end of
check_input
, add anotherif
to check if the user is pressingKEY_B
, and if so, callspawn_particle
. - Make the
spawn_particle
function. - In
spawn_particle
:- call
find_free_particle
.- This is the function that searches for an inactive particle. It returns
-1
to mean “all particles are active.” Otherwise, the number it returns is an index into theparticle_
arrays that is currently inactive.
- This is the function that searches for an inactive particle. It returns
- Move the return value into
s0
.- What do you have to do to “ask for permission” to use
s0
inspawn_particle
? Do that.
- What do you have to do to “ask for permission” to use
- If
s0
is not -1:- Set
particle_active[s0]
to 1. The constant 1. (This marks it as active.)- What type of array is
particle_active
? - If you didn’t get familiar with the “short” form of array indexing on lab 3, now’s a great time.
- What type of array is
- Print out
s0
followed by a space, for testing.print_str
is in the include file, so you can doprint_str " "
- Set
- call
Now to test:
- Assemble, run, click on the display.
- Hold down the
B
key on your keyboard.
You should see the following print in the MARS messages pane:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
91 92 93 94 95 96 97 98 99
That is, it prints the numbers 0 through 99, and then stops after 99.
If you get an alignment error, wrong kinda store bucko
If you get the output 16 16 16...
, you didn’t move s0
into the argument register before doing the print syscall
If you get the output 0 1 1 1 1...
, there are a few possible issues:
- you might be incorrectly multiplying the index by 4 before indexing
particle_active
- you might not be indexing
particle_active
at all - it’s an array, you have to index it
Tangent: we need more digits!
If we represented the particle positions and velocities using the same coordinate system as the emitter (X and Y both in the range [0, 63]), we would run into a problem: we don’t have enough precision for smooth movement.
If we want our particles to behave something like real physical objects, we need to be able to keep track of fractions of a pixel. Fractions usually mean we have to reach for floats, but there’s another technique called fixed-point numbers.
We might not have talked about this in class yet, but the idea is simple: we use an integer, but we pretend that some places are fractional. For this lab, we scale all the numbers up so that we have more digits of precision. For our particles, we’ll be using a scale of 100 to give us 2 digits of precision after the decimal point.
Instead of the particle coordinates ranging from 0 to 63, they will range from 0 to 6399. 6399 represents “63.99” in our coordinate system. We perform all calculations on this larger range, and then when we draw the particles, we divide by 100 to truncate off the “decimal places”: 6399 / 100 == 63.
So that’s what’s going on with the weird numbers in the PARTICLE_X_MAX
/PARTICLE_Y_MAX
constants, and why we’re multiplying and dividing by 100 in the upcoming code!
3.2. Finishing spawn_particle
Okay, let’s finish this. In spawn_particle
, delete the printing code now that we’ve confirmed it works. But leave the code that sets particle_active
!
After setting particle_active
, you need to set the other particle variables. Since all the other variables are .half
, you can multiply s0
by… what? (How many bytes is a .half
?) before accessing the other arrays.
Then set the properties as follows (remember, they’re .half
arrays):
particle_x[s0] = emitter_x * 100
particle_y[s0] = emitter_y * 100
particle_vx[s0] = 0
(you have a register that holds 0)particle_vy[s0] = 0
Now if you assemble, run, click the display, and hold down B
… nothing should happen. Well, nothing visible should happen. Importantly, it should not crash!
If you’re wondering why the heck we’re using
s0
, it’s cause we’ll need it for a later step.
4. Where the heck are the particles?
We’re creating them, they’re just not visible yet. Just like we had to write draw_emitter
, we have to draw the particles too. So:
- Uncomment the
jal draw_particles
line in the loop inmain
. - Make the
draw_particles
function. You’re gonna needs0
in this function too. draw_particles
should do this:
for(int i = 0; i < MAX_PARTICLES; i++) { // s0 is i
if(particle_active[i] != 0) {
// particle_x and particle_y are UNSIGNED arrays.
// So which should you use: lh or lhu?
display_set_pixel(particle_x[i] / 100, particle_y[i] / 100, COLOR_BLUE);
}
}
None of this code is new to you. You’ve done all this stuff before. Just remember to translate from the outside in. Write the for
loop first, then put the entire if
inside it. Do it!
I said, put the entire if
inside the for
loop. That means that you just skip inactive particles. you do not break out of the loop when you find an inactive particle.
Done correctly, you should now be able to “draw” on the screen by moving the emitter around and tapping or holding B
to place particles. The animation on the right shows this. You will eventually “run out of particles” to draw with, though, because you only have 100 of them. This is normal.
- If you can only draw one dot at a time, as in, you can’t hold B and move the emitter around:
- your
check_input
control flow is wrong. - It needs to be a sequence of
if
s, one for each key, not aswitch-case
.
- your
- If you cannot draw on the right side of the screen, only the left side:
- you are using the wrong flavor of
lh
for loading the particle position. particle_x
andparticle_y
are unsigned arrays.
- you are using the wrong flavor of
- If you just get a blue dot in the upper-left corner, or weird things like drawing diagonal lines:
- it’s most likely a problem in
spawn_particle
. draw_particles
just draws the particles whereverspawn_particle
placed them.
- it’s most likely a problem in
- If you can draw normally, but you get an extra blue dot in the upper-left corner:
- that’s probably all the inactive particles being drawn. Don’t draw them!
- Look at the pseudocode above - you should skip those by going to the next iteration of the loop.
You’re actually pretty close to being done!
5. Moving the particles
Here’s where the magic really happens. Once the particles start moving, it looks really cool.
- Uncomment the
jal update_particles
line in the loop inmain
. - Make the
update_particles
function. Yep, you needs0
here too. - Loop over all the particles, and only for the active ones (just like you did in
draw_particles
, I mean, it’s literally the samefor
loop andif
inside):particle_vy[i] += GRAVITY
particle_vx
andparticle_vy
are SIGNED, so which do you use,lh
orlhu
?
particle_x[i] += particle_vx[i]
(watch your use oflh
vslhu
on these)particle_y[i] += particle_vy[i]
(ditto)- Finally, if the particle’s position is “offscreen” (its X position is less than
PARTICLE_X_MIN
or greater thanPARTICLE_X_MAX
or its Y position is less thanPARTICLE_Y_MIN
or greater thanPARTICLE_Y_MAX
):- set
particle_active[i] = 0
to “despawn” it - then this particle can be recycled in the future!
- set
I believe in you. You can do this. Once done, you should be able to produce an infinite stream of particles that fall from the emitter, as shown in the animation to the right.
- If the particles still appear but don’t move:
- you’re not adding
GRAVITY
to the Y velocity correctly.
- you’re not adding
- If the particles move downward very slowly:
- then you’re probably just setting the Y velocity to
GRAVITY
instead of addingGRAVITY
to it.
- then you’re probably just setting the Y velocity to
- If no particles appear now when they did on the previous step:
- it likely means you are always despawning the particles.
- Try commenting out the
sb
that setsparticle_active
and see if they reappear. If that’s the case, then yeah, your conditionals for despawning particles are wrong.
- If the particles loop around to the top of the screen when they get to the bottom:
- yes it looks cool, but it likely means you are never despawning the particles. Your conditionals for that are wrong somehow.
- If the particles do strange things like flickering in and out, freezing in place, only moving when you hold B, etc:
- you are breaking out of the loop when you encounter an inactive particle instead of just skipping to the next iteration. the
if
forparticle_active
should be completely INSIDE the loop. This applies to bothdraw_particles
andupdate_particles
.
- you are breaking out of the loop when you encounter an inactive particle instead of just skipping to the next iteration. the
- If the particles move in strange ways (e.g. diagonally):
- it’s a really common mistake to mix up X and Y in this kind of code… make sure you are adding X and VX, and Y and VY, and not X and VY or something.
6. Randomizing velocity (making them “spray”)
Now for a little “victory lap.” This step is very quick and just makes things look a little better.
spawn_particle
currently starts every particle with a velocity vector of (0, 0) (sitting still). This makes them fall straight down. But if we give each particle an initial “kick” in some random direction, we can make it look like they’re “spraying” out of the emitter instead.
We’re going to use syscall 42 to generate some random numbers. It works like this:
a0
should always be 0.a1
is the upper bound of the random range.- it returns a value in the range
[0, a1)
- so if you seta1
to 10, you’ll get random numbers in the range[0, 9]
.
So here’s what you do in spawn_particle
:
- Instead of setting
particle_vx
to 0…- Use syscall 42 with an upper bound of
VEL_RANDOM_MAX
- Subtract
VEL_RANDOM_MAX_OVER_2
from the return value - Store that as
particle_vx
instead
- Use syscall 42 with an upper bound of
- Similarly, for
particle_vy
:- Use syscall 42 with an upper bound of
VEL_RANDOM_MAX
- You must set the
a0
anda1
arguments again!
- You must set the
- Subtract
VEL_RANDOM_MAX_OVER_2
from the return value - Subtract
GRAVITY
from the return value (just makes it look a little better) - Store that as
particle_vy
instead
- Use syscall 42 with an upper bound of
Doing this may reveal problems in your logic for detecting when particles should despawn off the left/right sides. So you may have to go back to update_particles
to fix that.
And that’s it.
Submitting
To submit:
- On Canvas, go to “Assignments” and click on this lab.
- Click “Start Assignment.”
- Under “File Upload,” click the “Browse” button and choose your
.asm
file. - Click “Submit Assignment.”
If you need to resubmit, that’s fine, just click “New Attempt” on the assignment page and upload it again.
Going Further
None of this stuff is required. It’s just fun ideas if you want to go further.
If you had fun making this, the cool thing about particle systems is how easy it is to change their behavior! Here are some ideas of things you could do:
- particle color
- add a
particle_color
array, and give each particle a random color when spawned- be sure not to use color 0 (black) or the particle will be invisible
- or instead of a random color, let the user pick a color with the Z, X, C keys
- keep track of particle age (spawn = 0, increment on each frame) and color differently based on age
- e.g. fade white, to yellow, to orange, to red, to brick, to dark grey
- that would be particularly effective if you set the
GRAVITY
constant to a negative number - now it looks like embers or sparks from a fire rising into the air!
- add a
- particle physics
- when particles hit left/right sides, instead of despawning them, limit their coordinates to the range
[PARTICLE_X_MIN, PARTICLE_X_MAX]
and negate their X velocity - now they bounce off the walls! - you can make them bounce off the bottom in a similar way but…
- only negating the Y velocity will make them bounce infinitely. which is neat but not realistic
- so you can negate the Y velocity and then multiply by a “restitution” constant in the range
[0, 100]
to make them gradually bounce lower and lower - eventually you will run out of particles though, so you’ll also have to despawn them some other way
- maybe when the absolute value of the result of the above multiplication is below some threshold?
- or just keep track of the age of each particle and despawn when their age reaches some value?
- wind can be implemented by just adding something to the X velocity of every particle on each frame - similar to gravity
- when particles hit left/right sides, instead of despawning them, limit their coordinates to the range
- particle behavior
- instead of being passive things that just fall down, maybe they can be “active” and move towards a point (the emitter?)
- this gets real tricky real fast because you have to perform trigonometry…
- instead of being passive things that just fall down, maybe they can be “active” and move towards a point (the emitter?)