generator and source files for the compudanzas.net site
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

#### 1696 lines 48 KiB Raw Permalink Blame History

 `# uxn tutorial: day 6, towards pong` `lang=en es->{tutorial de uxn día 6}` `this is the sixth section of the {uxn tutorial}! here we talk about how we can integrate everything that we have covered in order to create more complex subroutines and programs for the varvara computer.` ``` ``` `we base our discussion in a recreation of the classic pong game.` ``` ``` `besides using previous strategies and snippets of code, we cover strategies for drawing and controlling multi-tile sprites with the screen auto byte, and for checking collisions.` ``` ``` `# general logic` ``` ``` `even though pong might look simple and easy to program, once we analyze it we might find there are several aspects to account for. fortunately, most of them can be divided as different subroutines that we are able to discuss separately.` ``` ``` `we will tackle the following elements in order:` ``` ``` `* drawing the background` `* drawing and movement of the paddles` `* drawing and bouncing of the ball` ``` ``` `before discussing the paddles and the ball we will cover the screen auto byte that we mentioned on {uxn tutorial day 2}, as it will help us in the drawing of these elements.` ``` ``` `# drawing the background: repeating a tile` ``` ``` `in {uxn tutorial day 5} we discussed a way of creating a loop in order to repeat a 1bpp tile multiple times in a row.` ``` ``` `here we will expand that procedure in order to have it also repeated vertically in the whole screen.` ``` ``` `## setting up` ``` ``` `let's start with the following program as a template. it includes the data for a 1bpp sprite consisting of diagonal lines.` ``` ``` ````` `( hello-pong.tal )` ``` ``` `( devices )` `|00 @System [ &vector \$2 &pad \$6 &r \$2 &g \$2 &b \$2 ]` `|20 @Screen [ &vector \$2 &width \$2 &height \$2 &auto \$1 &pad \$1 &x \$2 &y \$2 &addr \$2 &pixel \$1 &sprite \$1 ]` `|80 @Controller [ &vector \$2 &button \$1 &key \$1 ]` ``` ``` `( macros )` `%RTN { JMP2r }` ``` ``` `( main program )` `|0100` `@setup` ` ( set system colors )` ` #2ce9 .System/r DEO2` ` #01c0 .System/g DEO2` ` #2ce5 .System/b DEO2` ``` ``` `BRK` ``` ``` `@tile-background 1122 4488 1122 4488` ````` ``` ``` `## repeating a tile in a row` ``` ``` `what's a procedure we could follow to repeat the drawing of a tile starting from x, and ending at a limit corresponding to x+width?` ``` ``` `one way would be something like:` ``` ``` `* draw tile in x` `* add 8 (the size of the tile) to x` `* is x less than the limit? if it is, repeat procedure, otherwise end` ``` ``` `### a first version` ``` ``` `let's say our initial x is 0000, our width is the screen width, and the tile we are drawing is tile-background.` ``` ``` `the limit for the loop, x+width, would be the screen width as well.` ``` ``` `the first step, drawing the tile in x would be:` ``` ``` ````` `;tile-background .Screen/addr DEO2 ( set tile address )` `#0000 .Screen/x DEO2 ( set initial x )` ``` ``` `#03 .Screen/sprite DEO ( draw 1bpp sprite with color 3 and 0 )` ````` ``` ``` `the second step, adding 8 to x, we know already:` ``` ``` ````` `.Screen/x DEI2 #0008 ADD2 ( add 8 to x )` `.Screen/x DEO2 ( store new x )` ````` ``` ``` `checking if x is less than the limit, jumping if it is, would be something like:` ``` ``` ````` `.Screen/x DEI2 ( get x )` `.Screen/width DEI2 ( get the limit )` `LTH2 ,&loop JCN ( jump if x is less than the limit )` ````` ``` ``` `integrating all of it, we would be able to get:` ``` ``` ````` `;tile-background .Screen/addr DEO2 ( set tile address )` `#0000 .Screen/x DEO2 ( set initial x )` ``` ``` ` &loop-x` ` #03 .Screen/sprite DEO ( draw 1bpp sprite with color 3 and 0 )` ` .Screen/x DEI2 #0008 ADD2 ( add 8 to x )` ` DUP2 ( duplicate new x )` ` .Screen/x DEO2 ( store new x )` ` .Screen/width DEI2 ( get the limit )` ` LTH2 ,&loop-x JCN ( jump if x is less than the limit )` ````` ``` ``` `note the use of DUP2 in order to avoid re-reading the value of x.` ``` ``` `this should work now! but let's discuss a nicer way of doing it :)` ``` ``` `### a second version, using the stack` ``` ``` `instead of reading the screen width and the x coordinate each time, we could use the stack to store and manipulate those values.` ``` ``` `after setting the tile address, we can push our limit (the screen width) and the initial value down into the stack:` ``` ``` ````` `.Screen/width DEI2 #0000 ( set limit and initial x )` ````` ``` ``` `we will be using that value at the top of the stack as the x coordinate.` ``` ``` `inside the loop, we can duplicate it to set it as the screen x, and to increment it.` ``` ``` `in between, we can send our sprite byte in order to draw the tile.` ``` ``` ````` ` &loop-x` ` DUP2 .Screen/x DEO2 ( set x coordinate )` ` #03 .Screen/sprite DEO ( draw 1bpp sprite with color 3 and 0 )` ` #0008 ADD2 ( increment x )` ````` ``` ``` `at this point, the stack has the new x at the top of the stack, and the screen width below.` ``` ``` `we can compare them in keep mode to mantain those values in the stack, and do our jump as before:` ``` ``` ````` `GTH2k ( is the width greater than x? aka is x less than the width ? )` `,&loop-x JCN ( jump if x is less than the limit )` ````` ``` ``` `when we are done with the loop, we have to POP both values.` ``` ``` `using this strategy, we would get the following loop:` ``` ``` ````` `;tile-background .Screen/addr DEO2 ( set tile address )` ``` ``` `.Screen/width DEI2 #0000 ( push limit and initial x )` ` &loop-x` ` DUP2 .Screen/x DEO2 ( set x coordinate )` ``` ``` ` #03 .Screen/sprite DEO ( draw 1bpp sprite with color 3 and 0 )` ``` ``` ` #0008 ADD2 ( increment x )` ` GTH2k ( is the width greater than x? aka is x less than the width ? )` ` ,&loop-x JCN ( jump if x is less than the limit )` `POP2 POP2 ( remove x and limit )` ````` ``` ``` `not only this is shorter code, but also it is faster because it performs less operations inside the loop.` ``` ``` `good to keep in mind!` ``` ``` `### complete program` ``` ``` `the following shows our program in context, completely filling the first row of our screen with our tile:` ``` ``` `=> ./img/screenshot_uxn-background-row.png screenshot showing the first row of the varvara screen filled with diagonal lines` ``` ``` ````` `( hello-pong.tal )` ``` ``` `( devices )` `|00 @System [ &vector \$2 &pad \$6 &r \$2 &g \$2 &b \$2 ]` `|20 @Screen [ &vector \$2 &width \$2 &height \$2 &auto \$1 &pad \$1 &x \$2 &y \$2 &addr \$2 &pixel \$1 &sprite \$1 ]` `|80 @Controller [ &vector \$2 &button \$1 &key \$1 ]` ``` ``` `( macros )` `%RTN { JMP2r }` ``` ``` `( main program )` `|0100` `@setup` ` ( set system colors )` ` #2ce9 .System/r DEO2` ` #01c0 .System/g DEO2` ` #2ce5 .System/b DEO2` ``` ``` ` ( draw background )` ` ;tile-background .Screen/addr DEO2 ( set tile address )` ``` ``` ` .Screen/width DEI2 #0000 ( set limit and initial x )` ` &loop-x` ` DUP2 .Screen/x DEO2 ( set x coordinate )` ` #03 .Screen/sprite DEO ( draw 1bpp sprite with color 3 and 0 )` ` #0008 ADD2 ( increment x )` ` GTH2k ( is the width greater than x? aka is x less than the width ? )` ` ,&loop-x JCN ( jump if x is less than the limit )` ` POP2 POP2 ( remove x and limit )` `BRK` ``` ``` `@tile-background 1122 4488 1122 4488` ````` ``` ``` `## repeating a row` ``` ``` `similar to what we just did: what's a procedure we could follow to repeat vertically a row starting from y, and ending at a limit corresponding to y+height?` ``` ``` `following the same strategy, we could do` ``` ``` `* draw row in y` `* add 8 (the size of the tile) to y` `* is y less than the limit? if it is, repeat procedure, otherwise end` ``` ``` `### the vertical loop` ``` ``` `to illustrate a little change, let's assume we want to have a margin at the top and bottom of the screen. we can define this margin as a macro:` ``` ``` ````` `%WALL-MARGIN { #0010 } ( margin at the top and bottom )` ````` ``` ``` `our initial y would be WALL-MARGIN, and our limit would be the screen height minus WALL-MARGIN.` ``` ``` `we can use the same structure as before, but using y:` ``` ``` ````` `;tile-background .Screen/addr DEO2 ( set tile address )` ``` ``` `.Screen/height DEI2 WALL-MARGIN SUB2 ( set limit )` `WALL-MARGIN ( set initial y )` ` &loop-y` ` DUP2 .Screen/y DEO2 ( set y coordinate )` ``` ``` ` ( - draw row here - )` ``` ``` ``` ``` ` #0008 ADD2 ( increment y )` ` GTH2k ( is the limit greater than y? aka is y less than the limit ? )` ` ,&loop-y JCN ( jump if y is less than the limit )` `POP2 POP2 ( remove y and limit )` ````` ``` ``` `### nested loops` ``` ``` `now that we have this structure, we can replace the "draw row here" comment with our previous horizontal loop:` ``` ``` ````` `;tile-background .Screen/addr DEO2 ( set tile address )` ``` ``` `.Screen/height DEI2 WALL-MARGIN SUB2 ( set limit )` `WALL-MARGIN ( set initial y )` ` &loop-y` ` DUP2 .Screen/y DEO2 ( set y coordinate )` ``` ``` ` ( draw row )` ` .Screen/width DEI2 #0000 ( set limit and initial x )` ` &loop-x` ` DUP2 .Screen/x DEO2 ( set x coordinate )` ``` ``` ` ( draw 1bpp sprite with color 3 and 0 )` ` #03 .Screen/sprite DEO` ``` ``` ` #0008 ADD2 ( increment x )` ` GTH2k ( is the width greater than x? aka is x less than the width ? )` ` ,&loop-x JCN ( jump if x is less than the limit )` ` POP2 POP2 ( remove x and limit )` ``` ``` ` #0008 ADD2 ( increment y )` ` GTH2k ( is the limit greater than y? aka is y less than the limit ? )` ` ,&loop-y JCN ( jump if y is less than the limit )` `POP2 POP2 ( remove y and limit )` ````` ``` ``` `note how, by making sure our inner loop leaves the stack clean after finishing, we can just safely put it in there: the values corresponding to the outer loop stay untouched in the stack.` ``` ``` `## draw-background subroutine` ``` ``` `now we can just wrap these nested loops inside a subroutine:` ``` ``` ````` `@draw-background ( -- )` ` ;tile-background .Screen/addr DEO2 ( set tile address )` ``` ``` ` .Screen/height DEI2 WALL-MARGIN SUB2 ( set limit )` ` WALL-MARGIN ( set initial y )` ` &loop-y` ` DUP2 .Screen/y DEO2 ( set y coordinate )` ``` ``` ` ( draw row )` ` .Screen/width DEI2 #0000 ( set limit and initial x )` ` &loop-x` ` DUP2 .Screen/x DEO2 ( set x coordinate )` ``` ``` ` ( draw 1bpp sprite with color 3 and 0 )` ` #03 .Screen/sprite DEO` ``` ``` ` #0008 ADD2 ( increment x )` ` GTH2k ( is the width greater than x? aka is x less than the width ? )` ` ,&loop-x JCN ( jump if x is less than the limit )` ` POP2 POP2 ( remove x and limit )` ``` ``` ` #0008 ADD2 ( increment y )` ` GTH2k ( is the limit greater than y? aka is y less than the limit ? )` ` ,&loop-y JCN ( jump if y is less than the limit )` ` POP2 POP2 ( remove y and limit )` `RTN` ````` ``` ``` `that we can simply call from our initialization subroutine:` ``` ``` ````` `;draw-background JSR2` ````` ``` ``` `=> ./img/screenshot_uxn-background-full.png screenshot showing the varvara screen covered in diagonal lines except for a margin at the top and bottom.` ``` ``` `nice!` ``` ``` `in {uxn tutorial appendix a} you can find a detailed discussion of how to generalize a procedure like this one into a draw-tiles subroutine that draws an arbitrary rectangle filled with a given tile.` ``` ``` `it goes into several possibilities for using uxntal in that abstract way: i'd say it's very interesting, but it is definitely out of scope for making the game :)` ``` ``` `# the screen auto byte` ``` ``` `the screen auto byte is an advanced feature of the screen device that allows us to easily draw multiple sprites with a single call.` ``` ``` `we didn't cover it on {uxn tutorial day 2} to avoid adding more complexity, and also because it really shines when using the keep mode that we just discussed on {uxn tutorial day 5}.` ``` ``` `## fields` ``` ``` `the left nibble of the byte corresponds to the auto byte length.` ``` ``` `the remaining four bits, numbered from right to left and from 0 to 3, are flags that indicate the following:` ``` ``` `+ ` `+
bitflag
3unused
` `+ ` `+ ` `+ ` `+ ` `+ ` `& * 3: unused` `& * 2: auto address` `& * 1: auto y` `& * 0: auto x` ``` ``` `## incrementing position` ``` ``` `the first application of the screen auto byte is to automatically increment the x or y coordinates of the screen device after drawing a sprite.` ``` ``` `### without auto byte` ``` ``` `for example, without the auto byte, if we wanted to draw a sprite just at the right of another, we would do something like the following, reading the screen x coordinate, incrementing it, and then saving it again:` ``` ``` ````` `;square .Screen/addr DEO2 ( set sprite address )` `#41 .Screen/sprite DEO ( draw sprite )` ``` ``` `( increment screen/x by 8 pixels: )` `.Screen/x DEI2 #0008 ADD2 .Screen/x` ``` ``` `#41 .Screen/sprite DEO ( draw sprite again )` ````` ``` ``` `the operation to increment the screen x is straightforward, but it's so common that the auto byte internalizes it.` ``` ``` `### with auto byte` ``` ``` `to replicate the same behavior, we can turn on the auto x flag, and instead do:` ``` ``` ````` `;square .Screen/addr DEO2 ( set sprite address )` ``` ``` `( set auto byte: auto x )` `#01 .Screen/auto` ``` ``` `#41 .Screen/sprite DEO ( draw sprite )` `#41 .Screen/sprite DEO ( draw sprite again )` ````` ``` ``` `if we hadn't set up the auto byte, the last two lines would be redundant. however, now that we included it, each of them draws a tile in a differet position.` ``` ``` `### auto byte and keep mode` ``` ``` `notice how in the previous code we are pushing twice into the stack the color and the screen sprite port address.` ``` ``` `with the keep mode we can use those values but without removing them from the stack!` ``` ``` `therefore, the last two lines of the previous snipped could be written instead using DEOk:` ``` ``` ````` `#41 .Screen/sprite DEOk ( draw sprite and keep values )` `DEO ( draw sprite again )` ````` ``` ``` `## incrementing address` ``` ``` `as we will see with the paddles and the ball of the game, in many times it makes sense to draw sprites consisting of several tiles.` ``` ``` `additionally, it makes sense to store these tiles contiguosly in memory, as we have been doing before.` ``` ``` `the auto address flag will allow us to increment the screen address short so that it points to the next address in memory of the tile that was just drawn.` ``` ``` `if what we drew was a 1bpp tile, the address will be incremented by 8 bytes, and if we drew a 2bpp tile the address will be incremented by 16 bytes.` ``` ``` `## multiple sprites` ``` ``` `finally, the length field of the auto byte will allow us to set how many extra sprites, besides the original one, we want to draw in a row.` ``` ``` `to reiterate, with the paddles and the ball below we will see how all these fields can be applied to draw multi-tile sprites!` ``` ``` `# the paddles` ``` ``` `we can think of the two paddles of the game as two rectangles, each one with its own x and y coordinates, and both with the same width and height.` ``` ``` `the x coordinate for each paddle can be constant, and the y coordinate should be a variable for sure.` ``` ``` `in this part we will see how to draw the paddles based on these parameters, and also recap how to change their y coordinates with the controller.` ``` ``` `## drawing the multi-tile paddles` ``` ``` `i want to use the paddles as an example of drawing a sprite composed of multiple tiles!` ``` ``` `### the data` ``` ``` `i used nasu to draw a paddle consisting of 2x3 tiles in 2bpp mode. i will number them from left to right and top to bottom in the following way:` ``` ``` ````` `0 1` `2 3` `4 5` ````` ``` ``` `=> https://100r.co/site/nasu.html 100R - nasu` ``` ``` `the resulting data is the following:` ``` ``` ````` `@paddle` ` &tile0 [ 3f 7f e7 c3 c3 c3 c3 c3 00 00 18 3c 3c 3c 3c 3c ]` ` &tile1 [ fc fe ff ff ff ff ff ff 00 00 00 00 00 00 06 06 ]` ` &tile2 [ c3 c3 c3 c3 e7 ff ff ff 3c 3c 3c 3c 18 00 00 00 ]` ` &tile3 [ ff ff ff ff ff ff ff ff 06 06 06 06 06 06 06 06 ]` ` &tile4 [ ff ff ff ff ff ff 7f 3f 00 00 00 00 00 00 00 00 ]` ` &tile5 [ ff ff ff ff ff ff fe fc 06 06 06 06 06 1e 3c 00 ]` ````` ``` ``` `one can get these numbers by reading the hexadecimal notation in nasu at the top right, first the left column and then the right column, or by using a tool like hexdump with the corresponding chr file:` ``` ``` ````` `\$ hexdump -C pong.chr` ````` ``` ``` `i drew the sprite using the blending mode 85 as indicated by nasu, but i will change it to c5 to draw it in the foreground.` ``` ``` `### paddle drawing subroutine` ``` ``` `let's build a subroutine that draws the 6 tiles of the paddle in the corresponding order.` ``` ``` `we could have the subroutine receiving as arguments the x and y position of its top left corner:` ``` ``` ````` `@draw-paddle ( x^ y^ -- )` ````` ``` ``` `but let's add a color byte for the sprite byte as well:` ``` ``` ````` `@draw-paddle ( x^ y^ color -- )` ````` ``` ``` `a reminder that we are using the convention of adding a caret (^) after the name of a value to indicate it's a short, and an asterisk (*) to indicate it's a short working as a pointer (i.e. an address in program memory)` ``` ``` `on one hand this second version would allow us to change colors when e.g. hitting the ball, but more importantly this will allow us to clear the paddle before moving it, as we have done in previous days.` ``` ``` `in principle the subroutine should be straightforward: we have to set the x and y coordinates of each of the tiles, relative to the given x and y coordinates, and draw them with the given color.` ``` ``` `if we didn't want to use the screen auto byte, we could do something like the following:` ``` ``` `* draw tile 0, then add 8 to x` `* draw tile 1, then subtract 8 to x, and add 8 to y` `* draw tile 2, then add 8 to x` `* draw tile 3, then subtract 8 to x, and add 8 to y` `* draw tile 4, then add 8 to x` `* draw tile 5` ``` ``` `however, as that approach or similar ones would imply a lot of operations, it makes sense to use the auto byte.` ``` ``` `for this, we can configure it so that we draw two tiles in a row, automatically incrementing the screen address, and also automatically incrementing the screen y coordinate after being done with a row:` ``` ``` ````` `@draw-paddle ( x^ y^ color -- )` ` ( save color )` ` STH` ``` ``` ` ( set initial y and x )` ` .Screen/y DEO2` ` .Screen/x DEO2` ``` ``` ` ( set first sprite address )` ` ;paddle-sprite .Screen/addr DEO2` ``` ``` ` ( auto byte: length: +1 sprite )` ` ( set auto addr and auto y )` ` #16 .Screen/auto DEO` ``` ``` ` ( get color from return stack: )` ` STHr` ` ( draw three rows: )` ` .Screen/sprite DEOk DEOk DEO` `RTN` ````` ``` ``` `that's it!` ``` ``` `note the use of the keep mode at the end: DEOk will output the color to the screen sprite port, but it will keep both of these parameters in the stack so that we can re-use them!` ``` ``` `now that the subroutine is ready, we can call it in e.g. the following way and get our paddle drawn:` ``` ``` ````` `#0008 #0008 #c5 ;draw-paddle JSR2` ````` ``` ``` `=> ./img/screenshot_uxn-pong-paddle.png screenshot of the paddle drawn over the background` ``` ``` `## variables and constants for the paddles` ``` ``` `let's reserve some space in the zero page for the x and y coordinates of each paddle.` ``` ``` ````` `( zero page )` `|0000` ` @left [ &x \$2 &y \$2 ]` ` @right [ &x \$2 &y \$2 ]` ````` ``` ``` `we mentioned early that the x coordinate is constant; however, if we make it a variable then we can dinamically assign the x position of the paddles (especially the right one) depending on the size of the screen.` ``` ``` `we can have a couple of macros to hold the dimensions and color of the paddles in order to use them later:` ``` ``` ````` `%PADDLE-WIDTH { #0010 } ( 2 tiles )` `%PADDLE-HEIGHT { #0018 } ( 3 tiles )` `%PADDLE-COLOR { #c5 }` ````` ``` ``` `a margin to separate the paddles from the borders could be nice as well:` ``` ``` ````` `%MARGIN { #0010 }` ````` ``` ``` `finally, let's bring back our HALF2 macro from previous days:` ``` ``` ````` `%HALF2 { #01 SFT2 } ( short -- short/2 )` ````` ``` ``` `### initialize positions` ``` ``` `now we can initialize the positions of the paddles.` ``` ``` `for the left x, we can just assign a constant value:` ``` ``` ````` `MARGIN .left/x STZ2` ````` ``` ``` `for the right x, we can subtract the margin and the width of the paddle from the screen width:` ``` ``` ````` `.Screen/width DEI2` `MARGIN SUB2 PADDLE-WIDTH SUB2` `.right/x STZ2` ````` ``` ``` `for centering the y coordinates we can subtract the paddle height from the screen height, and then divide over two:` ``` ``` ````` `.Screen/height DEI2 PADDLE-HEIGHT SUB2 HALF2` `DUP2` `.left/y STZ2` `.right/y STZ2` ````` ``` ``` `### draw paddles` ``` ``` `in order to draw each paddle, we can do the following procedure inside our on-frame screen vector:` ``` ``` ````` `( draw paddles )` `.left/x LDZ2 .left/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` `.right/x LDZ2 .right/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` ````` ``` ``` `### the program so far` ``` ``` `omitting the definition of the draw-background and draw-paddle subroutines, and as a way of having a checkpoint, right now our program would look like the following:` ``` ``` `=> ./img/screenshot_uxn-pong-paddles.png screenshot of the two paddles, vertically centered and with the same margin relative to the sides` ``` ``` ````` `( hello-pong.tal )` ``` ``` `( devices )` `|00 @System [ &vector \$2 &pad \$6 &r \$2 &g \$2 &b \$2 ]` `|20 @Screen [ &vector \$2 &width \$2 &height \$2 &auto \$1 &pad \$1 &x \$2 &y \$2 &addr \$2 &pixel \$1 &sprite \$1 ]` `|80 @Controller [ &vector \$2 &button \$1 &key \$1 ]` ``` ``` `( macros )` `%RTN { JMP2r }` `%HALF2 { #01 SFT2 } ( short -- short/2 )` ``` ``` `( constants )` `%PADDLE-WIDTH { #0010 } ( 2 tiles )` `%PADDLE-HEIGHT { #0018 } ( 3 tiles )` `%PADDLE-COLOR { #c5 }` `%MARGIN { #0010 }` `%WALL-MARGIN { #0010 } ( margin at the top and bottom )` ``` ``` `( zero page )` `|0000` ` @left [ &x \$2 &y \$2 ]` ` @right [ &x \$2 &y \$2 ]` ``` ``` `( main program )` `|0100` `@setup` ` ( set system colors )` ` #2ce9 .System/r DEO2` ` #01c0 .System/g DEO2` ` #2ce5 .System/b DEO2` ``` ``` ` ( set screen vector )` ` ;on-frame .Screen/vector DEO2` ``` ``` ` ( draw background )` ` ;draw-background JSR2` ``` ``` ` ( initialize paddles )` ` MARGIN .left/x STZ2` ` .Screen/width DEI2` ` MARGIN SUB2 PADDLE-WIDTH SUB2` ` .right/x STZ2` ``` ``` ` .Screen/height DEI2 PADDLE-HEIGHT SUB2` ` HALF2 DUP2` ` .left/y STZ2` ` .right/y STZ2` ``` ``` `BRK` ``` ``` `@on-frame ( -> )` ` ( draw paddles )` ` .left/x LDZ2 .left/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` ` .right/x LDZ2 .right/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` `BRK` ````` ``` ``` `## paddle movement` ``` ``` `for the paddle movement we can go back to previous examples of moving a sprite. the process we have followed is:` ``` ``` `* clear sprite from current position` `* update position` `* draw sprite in new position` ``` ``` `### clear or draw sprite` ``` ``` `we have already the process for drawing our paddles:` ``` ``` ````` `( draw paddles )` `.left/x LDZ2 .left/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` `.right/x LDZ2 .right/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` ````` ``` ``` `in order to clear them, we can do the same but using a sprite byte corresponding to clear the tile in the foreground:` ``` ``` ````` `( clear paddles )` `.left/x LDZ2 .left/y LDZ2 CLEAR-COLOR ;draw-paddle JSR2` `.right/x LDZ2 .right/y LDZ2 CLEAR-COLOR ;draw-paddle JSR2` ````` ``` ``` `where CLEAR-COLOR in this case would be:` ``` ``` ````` `%CLEAR-COLOR { #40 } ( clear sprite from the foreground )` ````` ``` ``` `this is a good reminder to review the tables for the sprite bytes in {uxn tutorial day 2}!` ``` ``` `### update position` ``` ``` `to update the position of our paddles, we can resort to the hello-moving-sprite.tal example from {uxn tutorial day 4}.` ``` ``` `we can use up and down arrows to change the position of the left paddle, and ctrl and alt (A and B) buttons to change the position of the right paddle.` ``` ``` `we can have a macro to define the paddle speed, i.e. how much we'll add or subtract when moving each frame:` ``` ``` ````` `%PADDLE-SPEED { #0001 }` ````` ``` ``` `all of this can go inside its own subroutine for readability purposes:` ``` ``` ````` `@update-paddles ( -- )` ` &left` ` ( left paddle: up and down buttons )` ` .Controller/button DEI` ` DUP #10 AND ( check bit for up )` ` ,&left-up JCN` ` DUP #20 AND ( check bit for down )` ` ,&left-down JCN` ``` ``` ` ,&right JMP ( jump if neither of them were pressed )` ``` ``` ` &left-up` ` .left/y LDZ2 PADDLE-SPEED SUB2 .left/y STZ2` ` ,&right JMP` ` &left-down` ` .left/y LDZ2 PADDLE-SPEED ADD2 .left/y STZ2` ` ,&right JMP` ``` ``` ` &right` ` ( right paddle: ctrl/A and alt/B buttons )` ` DUP #01 AND ( check bit for A )` ` ,&right-up JCN` ` DUP #02 AND ( check bit for B )` ` ,&right-down JCN` ``` ``` ` ,&end JMP ( jump if neither of them were pressed )` ``` ``` ` &right-up` ` .right/y LDZ2 PADDLE-SPEED SUB2 .right/y STZ2` ` ,&end JMP` ` &right-down` ` .right/y LDZ2 PADDLE-SPEED ADD2 .right/y STZ2` ``` ``` ` &end` ` POP ( pop duplicate value of button )` `RTN` ````` ``` ``` `### complete procedure` ``` ``` `integrating everything, our on-frame subroutine would look like the following.` ``` ``` `now we are able to move our paddles!` ``` ``` ````` `@on-frame ( -> )` ` ( clear paddles )` ` .left/x LDZ2 .left/y LDZ2 CLEAR-COLOR ;draw-paddle JSR2` ` .right/x LDZ2 .right/y LDZ2 CLEAR-COLOR ;draw-paddle JSR2` ``` ``` ` ( update paddles )` ` ;update-paddles JSR2` ``` ``` ` ( draw paddles )` ` .left/x LDZ2 .left/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` ` .right/x LDZ2 .right/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` `BRK` ````` ``` ``` `note that we are able to move the paddles beyond the limits of the screen.` ``` ``` `i invite you to modify the update-paddles subroutine so that there's a limit in the paddles movement. in {uxn tutorial day 4} we discussed some possible strategies for achieving it :)` ``` ``` `# the ball` ``` ``` `now let's get the ball rolling!` ``` ``` `here we'll work again with a multi-tile sprite drawn relative to x and y variables for its top left corner.` ``` ``` `additionally, we'll use this section to talk about strategies for collision detection, with the walls and the paddles.` ``` ``` `## drawing the ball` ``` ``` `i used nasu to draw a ball composed of 2x2 2bpp tiles, ordered in the following way:` ``` ``` ````` `0 1` `2 3` ````` ``` ``` `here's its data:` ``` ``` ````` `@ball-sprite` ` &tile0 [ 03 0f 1f 39 70 70 f9 ff 00 00 00 06 0f 0f 06 00 ]` ` &tile1 [ c0 f0 f8 fc fe fe ff ff 00 00 00 00 08 0c 06 06 ]` ` &tile2 [ ff ff 7f 7f 3f 1f 0f 03 00 00 00 00 18 0f 01 00 ]` ` &tile3 [ ff ff fe fe fc f8 f0 c0 06 06 0c 1c 38 f0 c0 00 ]` ````` ``` ``` `we can define a couple of macros to refer to its parameters:` ``` ``` ````` `%BALL-SIZE { #0010 } ( 2 tiles per side )` `%BALL-COLOR { #c5 }` ````` ``` ``` `### subroutine for drawing the ball` ``` ``` `as we'll be drawing a single ball, we can write its drawing subroutine such that it takes its coordinates from the zero-page instead of getting them as arguments in the stack.` ``` ``` `in our zero page we can define the labels for the coordinates:` ``` ``` ````` `@ball [ &x \$2 &y \$2 ]` ````` ``` ``` `and then in our setup subroutine we can assign values to them, e.g. at the middle of the screen:` ``` ``` ````` `( inside setup )` `( initialize ball )` `.Screen/width DEI2 BALL-SIZE SUB2` `HALF2` `.ball/x STZ2` ``` ``` `.Screen/height DEI2 BALL-SIZE SUB2` `HALF2` `.ball/y STZ2` ````` ``` ``` `the coordinates are ready, so now we can use them inside our subroutine.` ``` ``` `let's have the subroutine receive the color as an argument, so that we can clear the ball like we do with the paddles:` ``` ``` ````` `@draw-ball ( color -- )` ` ( set initial x and y )` ` .ball/x LDZ2 .Screen/x DEO2` ` .ball/y LDZ2 .Screen/y DEO2` ``` ``` ` ( set sprite address )` ` ;ball-sprite .Screen/addr DEO2` ``` ``` ` ( auto byte: draw +1 sprite )` ` ( set auto addr and auto y )` ` #16 .Screen/auto DEO` ``` ``` ` ( set color from working stack: )` ` .Screen/sprite` ` ( draw two rows: )` ` DEOk DEO` `RTN` ````` ``` ``` `notice how we are using a very similar approach to the draw-paddle subroutine!` ``` ``` `in order to draw it, we'd just need to do:` ``` ``` ````` `( draw ball )` `BALL-COLOR ;draw-ball JSR2` ````` ``` ``` `=> ./img/screenshot_uxn-pong-paddles-and-ball.png screenshot of the screen showing the paddles in their horizontal position but at different heights, and the ball completely centered in the screen.` ``` ``` `## ball movement` ``` ``` `for the movement of the ball, we'll follow the same structure as before:` ``` ``` `* clear the ball in the current position` `* update its position` `* draw the ball in the new position` ``` ``` `it would look something like the following, and it could sit along the equivalent procedures for the paddles inside the on-frame subroutine:` ``` ``` ````` `( inside on-frame )` `( clear ball )` `CLEAR-COLOR ;draw-ball JSR2` ``` ``` `( update ball )` `;update-ball JSR2` ``` ``` `( draw ball )` `BALL-COLOR ;draw-ball JSR2` ````` ``` ``` `now let's discuss how to build that update-ball subroutine :)` ``` ``` `### accounting for the change of direction` ``` ``` `besides our variables for keeping track of the position of the ball, we should be able to keep track of the per-axis direction it is moving.` ``` ``` `one approach could be to have a flag for each x and y that indicates if we should increment or decrement them.` ``` ``` `another approach could be to have a speed variable for each x and y, that gets changed according to the direction we want the ball to go.` ``` ``` `we will use this latter approach with the speed as it will help us discuss some perks of unsigned integer arithmetic!` ``` ``` `we include these variables in our zero page, complementing the x and y we had already:` ``` ``` ````` `@ball [ &x \$2 &y \$2 &speed-x \$2 &speed-y \$2 ]` ````` ``` ``` `### different directions` ``` ``` `if, for example, we initialize speed-x with 1:` ``` ``` ````` `#0001 .ball/speed-x STZ2` ````` ``` ``` `we can make the ball move to the right by doing:` ``` ``` ````` `( inside update-ball )` `.ball/speed-x LDZ2 ( get speed-x )` `.ball/x LDZ2 ( get x )` `ADD2 ( add them together )` `.ball/x STZ2 ( store new x )` ````` ``` ``` `in order to move to the left, we might think that we should replace ADD2 with SUB2. and yes, we could do that.` ``` ``` `but, to leave our code as it is now: is there a value of speed-x that will make x get smaller when adding them together?` ``` ``` `in other contexts, one might say, "-1"!` ``` ``` `but we haven't used negative signs here in uxn; we can't!` ``` ``` `then, is there a positive value of speed-x that will make x get smaller when adding them together?` ``` ``` `normally we would think there isn't and that the question doesn't make sense!` ``` ``` `however, here we are constrained by 8 or 16 bits. and what does that imply?` ``` ``` `for example, if we have the number ffff (16 bits, all are ones), and we add 0001, what do we get?` ``` ``` ````` ` 1111 1111 1111 1111` ` + 0000 0000 0000 0001` ` ---------------------` ` 1 0000 0000 0000 0000` ````` ``` ``` `ok, it's a bigger number, but the 1 at the left sits outside the 16 bits! in other contexts this would be called the carry bit.` ``` ``` `in uxn, the result of adding ffff and 0001 is 0000: we say we are overflowing the 16 bits.` ``` ``` `let's look at it the other way around: if we have 0001, and we add ffff, we get 0000, that is 1 less than 1!` ``` ``` `if we have 0002, and we add ffff:` ``` ``` ````` ` 0000 0000 0000 0010` ` + 1111 1111 1111 1111` ` --------------------` ` 1 0000 0000 0000 0001` ````` ``` ``` `we get 0001, that is 1 less than 2!` ``` ``` `in general, if we add ffff to a 16 bits number, we'll get a value that is 1 less than itself.` ``` ``` `therefore we can think that ffff is like a "-1"!` ``` ``` `to get other "negative numbers", let's observe the following: if we subtract 1 from ffff, we get fffe. what happens if we add it to 2?` ``` ``` ````` ` 0000 0000 0000 0010: 0002` ` + 1111 1111 1111 1110: fffe` ` ---------------------` ` 1 0000 0000 0000 0000: 0000` ````` ``` ``` `we get 0! fffe works effectively as "-2"!` ``` ``` `we could continue in that way getting more and more "negative" numbers that works thanks to the constrained size of computer memory.` ``` ``` `going back to our code, if we initialize our speed with:` ``` ``` ````` `#ffff .ball/speed-x STZ2` ````` ``` ``` `and then we use exactly the same code to update the position:` ``` ``` ````` `( inside update-ball )` `.ball/speed-x LDZ2 ( get speed-x )` `.ball/x LDZ2 ( get x )` `ADD2 ( add them together )` `.ball/x STZ2 ( store new x )` ````` ``` ``` `we will have decreased the position by 1!` ``` ``` `it might make sense to set these values as macros:` ``` ``` ````` `%BALL-POSITIVE-SPEED { #0001 } ( +1 )` `%BALL-NEGATIVE-SPEED { #ffff } ( -1 )` ````` ``` ``` `### implementing the ball movement` ``` ``` `based on what we just discussed, we can start our update-ball subroutine with the following:` ``` ``` ````` `@update-ball ( -- )` ` ( get speed-x and x )` ` .ball/speed-x LDZ2 .ball/x LDZ2` ` ADD2 ( add them together )` ` .ball/x STZ2 ( store new x )` ``` ``` ` ( get speed-y and y )` ` .ball/speed-y LDZ2 .ball/y LDZ2` ` ADD2 ( add them together )` ` .ball/y STZ2 ( store new y )` `RTN` ````` ``` ``` `if we complement our setup routine with the initial speeds, we'll be able to see the ball moving:` ``` ``` ````` `( inside setup )` `( initialize ball )` `.Screen/width DEI2 BALL-SIZE SUB2` `HALF2 .ball/x STZ2` `.Screen/height DEI2 BALL-SIZE SUB2` `HALF2 .ball/y STZ2` ``` ``` `( initialize ball speed )` `BALL-POSITIVE-SPEED .ball/speed-x STZ2` `BALL-POSITIVE-SPEED .ball/speed-y STZ2` ````` ``` ``` `woohoo! it moves, but for the moment it flies away :)` ``` ``` `## collisions with the walls` ``` ``` `we have defined the general way of updating the position of the ball given its speed in x and y.` ``` ``` `now let's see how to implement the classical "bounce"!` ``` ``` `first, let's start with the walls at the top and bottom of the screen; remembering that there is a margin (WALL-MARGIN) between the actual border of the screen, and the walls.` ``` ``` `in order to perform these collision detections, we'd need to check the y coordinate of the ball only.` ``` ``` `as always, there are many ways to achieve this. one could be:` ``` ``` `* check if the ball is hitting either wall` `* if it is, then invert the speed` ``` ``` `another one:` ``` ``` `* check if the ball is hitting the top wall` `* if it is, then set a positive speed` `* if not, check if the ball is hitting the bottom wall` `* if it is, then set a negative speed` ``` ``` `in other languages it is probably easier to write the first one, but here we'll stick with the second: for clarity and because of the way we'll have to do the checks.` ``` ``` `in any case, it can be a good exercise for you to try to figure out how to "invert" the speed with a single arithmetic or logic operation!` ``` ``` `hint: look again at the bitwise masks discussed on {uxn tutorial day 3} :)` ``` ``` `### top wall` ``` ``` `if the ball is hitting the top wall, it means its y coordinate is less than the y coordinate of the wall.` ``` ``` `considering that there's a margin at the top, we can do this check as follows:` ``` ``` ````` `( inside update-ball )` ` &check-top-wall` ` .ball/y LDZ2` ` WALL-MARGIN` ` LTH2 ( is ball-y less than the margin? )` ` ,&set-positive-speed JCN` ` ,&check-bottom-wall JMP` ``` ``` ` &set-positive-speed` ` BALL-POSITIVE-SPEED .ball/speed-y STZ2` ` ,&continue JMP` ` &check-bottom-wall` ````` ``` ``` `### bottom wall` ``` ``` `here the procedure would be similar, but considering the size of the ball.` ``` ``` `we'd like to know if the y coordinate, plus the size of the ball, is greater than the y coordinate of the bottom wall.` ``` ``` `the y coordinate of the bottom wall would be the height of the screen, less the wall margin:` ``` ``` ````` `(inside update ball )` ` &check-bottom-wall` ` .ball/y LDZ2 BALL-SIZE ADD2 ( y + ball size )` ` .Screen/height DEI2` ` WALL-MARGIN SUB2 ( height - margin )` ` GTH2 ( is the ball-y greater than the wall-y? )` ` ,&set-negative-speed JCN` ` ,&continue JMP` ``` ``` ` &set-negative-speed` ` BALL-NEGATIVE-SPEED .ball/speed-y STZ2` ` &continue` ````` ``` ``` `### update-ball code so far` ``` ``` `our update-ball subroutine looks like the following right now:` ``` ``` ````` `@update-ball ( -- )` ` ( update x )` ` ( get speed-x and x )` ` .ball/speed-x LDZ2 .ball/x LDZ2` ` ADD2 ( add them together )` ` .ball/x STZ2 ( store new x )` ``` ``` ` ( update y )` ` ( get speed-y and y )` ` .ball/speed-y LDZ2 .ball/y LDZ2` ` ADD2 ( add them together )` ` .ball/y STZ2 ( store new y )` ``` ``` ` ( check collisions with walls )` ` &check-top-wall` ` .ball/y LDZ2` ` WALL-MARGIN` ` LTH2 ( is ball-y less than the margin? )` ` ,&set-positive-speed JCN` ` ,&check-bottom-wall JMP` ``` ``` ` &set-positive-speed` ` BALL-POSITIVE-SPEED .ball/speed-y STZ2` ` ,&continue JMP` ``` ``` ` &check-bottom-wall` ` .ball/y LDZ2 BALL-SIZE ADD2 ( y + ball size )` ` .Screen/height DEI2` ` WALL-MARGIN SUB2 ( height - margin )` ` GTH2 ( is the ball y greater than the wall y? )` ` ,&set-negative-speed JCN` ` ,&continue JMP` ``` ``` ` &set-negative-speed` ` BALL-NEGATIVE-SPEED .ball/speed-y STZ2` ` &continue` `RTN` ````` ``` ``` `you can test it using different initial speed-y inside setup. the ball should be bouncing at the top and bottom now! :)` ``` ``` `## collisions with the paddles` ``` ``` `let's work with what we just did, and adapt it for bouncing with the paddles!` ``` ``` `### left paddle` ``` ``` `first of all, we can identify if the x coordinate of the ball would be hitting the left paddle.` ``` ``` `for this, we can check if x is less than the sum of the margin and paddle width.` ``` ``` ````` `( inside update-ball )` ` &check-left-paddle` ` .ball/x LDZ2` ` MARGIN PADDLE-WIDTH ADD2` ` LTH2 ( is ball-x less than the margin + paddle-width? )` ` ,&x-in-left JCN` ` ,&check-right-paddle JMP` ``` ``` ` &x-in-left` ` ( ... )` ``` ``` ` &check-right-paddle` ````` ``` ``` `once we know that's true, we can see if the ball is within vertical reach of the paddle; the y coordinate of the ball has to be within a certain range relative to the y coordinate of the ball.` ``` ``` `specifically, if we want the ball to be able to bounce when any part of the ball hits any part of the paddle, the y coordinate of the ball has to be:` ``` ``` `* greater than the paddle y coordinate minus the ball height AND` `* less than the paddle y coordinate plus the paddle height` ``` ``` `if those two conditions are met, then we can set a positive speed for x:` ``` ``` ````` `( inside update-ball )` ` &x-in-left` ` .ball/y LDZ2 DUP2` ` .left/y LDZ2 BALL-SIZE SUB2 GTH2 ( first flag ) STH` ` .left/y LDZ2 PADDLE-HEIGHT ADD2 LTH2 ( second flag )` ` STHr ( retrieve first flag )` ` AND ( AND the two flags together )` ` ,&bounce-left JCN` ````` ``` ``` `where bounce-left would be:` ``` ``` ````` ` &bounce-left` ` BALL-POSITIVE-SPEED .ball/speed-x STZ2` ` ,&finish JMP` ````` ``` ``` `and what happens if both conditions are not met at the same time?` ``` ``` `we can let the ball keep moving, but checking that it hasn't crossed the left wall, by comparing with 0000.` ``` ``` `the whole x-in-left code would end up looking like:` ``` ``` ````` `( inside update-ball )` ` &x-in-left` ` .ball/y LDZ2 DUP2` ` .left/y LDZ2 BALL-SIZE SUB2 GTH2 ( first flag ) STH` ` .left/y LDZ2 PADDLE-HEIGHT ADD2 LTH2 ( second flag )` ` STHr ( retrieve first flag )` ` AND ( AND the two flags together )` ` ,&bounce-left JCN` ``` ``` ` .ball/x LDZ2 #0000 NEQ2 ( has it reached the wall ? )` ` ,&finish JCN` ``` ``` ` &reset-left` ` ( here you can increase the score of` ` the right paddle )` ` ;reset JSR2` ` ,&finish JMP` ``` ``` ` &bounce-left` ` BALL-POSITIVE-SPEED .ball/speed-x STZ2` ` ,&finish JMP` ``` ``` ` &check-right-paddle` ````` ``` ``` `"finish" would be a label at the end of the subroutine, and "reset" is a subroutine that we will discuss later.` ``` ``` `this approach of comparing with 0000 is the easiest, but keep in mind that it might not work if you change the ball speed: it could happen that it crosses the wall but with an x coordinate that is never equal to 0.` ``` ``` `we can't really check if the x coordinate is less than 0, because as we discussed above, that would actually be a number close to ffff.` ``` ``` `if we checked for the x coordinate being less than ffff, then every possible value would turn on the comparison flag!` ``` ``` `this can be another good exercise for you: how would you check if the ball has crossed the left wall regardless of its speed?` ``` ``` `### right paddle` ``` ``` `for the right paddle we will do the same as above, but changing the comparisons relative to the x coordinate of the ball: we will use the screen width as a reference for the right wall, and from there we'll subtract the margin and widths.` ``` ``` ````` ` &check-right-paddle` ` .ball/x LDZ2 BALL-SIZE ADD2 ( ball-x + ball-size )` ` .Screen/width DEI2 MARGIN SUB2 PADDLE-WIDTH SUB2` ` GTH2 ( is ball's right coordinate greater than the screen width - margin - paddle-width? )` ` ,&x-in-right JCN` ` ,&finish JMP` ``` ``` ` &x-in-right` ` .ball/y LDZ2 DUP2` ` .right/y LDZ2 BALL-SIZE SUB2 GTH2 ( first flag ) STH` ` .right/y LDZ2 PADDLE-HEIGHT ADD2 LTH2 ( second flag )` ` STHr ( retrieve first flag )` ` AND ( AND the two flags together )` ` ,&bounce-right JCN` ``` ``` ` .ball/x LDZ2` ` .Screen/width DEI2 NEQ2 ( has it reached the wall ? )` ` ,&finish JCN` ``` ``` ` &reset-right` ` ( here you can increase the score` ` of the left paddle )` ` ;reset JSR2` ` ,&finish JMP` ``` ``` ` &bounce-right` ` BALL-NEGATIVE-SPEED .ball/speed-x STZ2` ` ,&finish JMP` ``` ``` ` &finish` `RTN` ````` ``` ``` `that should be it! you can find the complete update-ball subroutine below!` ``` ``` `in order to be able to assemble and run the game, let's define the reset subroutine!` ``` ``` `## reset` ``` ``` `here we'll just define a reset subroutine that returns the ball to the middle of the screen without altering its speed:` ``` ``` ````` `@reset ( -- )` ` ( initialize ball )` ` .Screen/width DEI2 BALL-SIZE SUB2` ` HALF2 .ball/x STZ2` ` .Screen/height DEI2 BALL-SIZE SUB2` ` HALF2 .ball/y STZ2` `RTN` ````` ``` ``` `it would be interesting to have some mechanism to also change the speed: maybe based on the framecount, in the position of the paddles, or whatever else you choose.` ``` ``` `# the complete program` ``` ``` `here's all of the code we wrote today!` ``` ``` `=> ./img/screencap_uxn-pong.gif animated gif showing pong in action: the paddles move, the ball bounces in the top and bottom walls and in the paddles, and the ball restarts from the middle when the ball hits any side.` ``` ``` `## setup` ``` ``` ````` `( hello-pong.tal )` ``` ``` `( devices )` `|00 @System [ &vector \$2 &pad \$6 &r \$2 &g \$2 &b \$2 ]` `|20 @Screen [ &vector \$2 &width \$2 &height \$2 &auto \$1 &pad \$1` ` &x \$2 &y \$2 &addr \$2 &pixel \$1 &sprite \$1 ]` `|80 @Controller [ &vector \$2 &button \$1 &key \$1 ]` ``` ``` `( macros )` `%RTN { JMP2r }` `%HALF2 { #01 SFT2 } ( short -- short/2 )` `%DOUBLE2 { #10 SFT2 }` ``` ``` `( constants )` `%PADDLE-WIDTH { #0010 } ( 2 tiles )` `%PADDLE-HEIGHT { #0018 } ( 3 tiles )` `%PADDLE-COLOR { #c5 }` `%PADDLE-SPEED { #0001 }` `%BALL-SIZE { #0010 } ( 2 tiles )` `%BALL-COLOR { #c5 }` `%BALL-POSITIVE-SPEED { #0001 }` `%BALL-NEGATIVE-SPEED { #ffff }` `%CLEAR-COLOR { #40 }` `%MARGIN { #0010 } ( left and right )` `%WALL-MARGIN { #0010 } ( top and bottom )` ``` ``` `( zero page )` `|0000` ` @left [ &x \$2 &y \$2 ]` ` @right [ &x \$2 &y \$2 ]` ` @ball [ &x \$2 &y \$2 &speed-x \$2 &speed-y \$2 ]` ``` ``` `( setup )` `|0100` `@setup ( -> )` ` ( set system colors )` ` #2ce9 .System/r DEO2` ` #01c0 .System/g DEO2` ` #2ce5 .System/b DEO2` ``` ``` ` ( set screen vector )` ` ;on-frame .Screen/vector DEO2` ``` ``` ` ( draw background )` ` ;draw-background JSR2` ``` ``` ` ( initialize paddles )` ` MARGIN .left/x STZ2` ` .Screen/width DEI2` ` MARGIN SUB2 PADDLE-WIDTH SUB2` ` .right/x STZ2` ``` ``` ` .Screen/height DEI2 PADDLE-HEIGHT SUB2` ` HALF2 DUP2` ` .left/y STZ2` ` .right/y STZ2` ``` ``` ` ( initialize ball )` ` ;reset JSR2` ``` ``` ` ( initialize ball speed )` ` BALL-NEGATIVE-SPEED .ball/speed-x STZ2` ` BALL-POSITIVE-SPEED .ball/speed-y STZ2` ``` ``` `BRK` ````` ``` ``` `## on-frame` ``` ``` ````` `@on-frame ( -> )` ` ( clear paddles )` ` .left/x LDZ2 .left/y LDZ2 CLEAR-COLOR ;draw-paddle JSR2` ` .right/x LDZ2 .right/y LDZ2 CLEAR-COLOR ;draw-paddle JSR2` ``` ``` ` ( clear ball )` ` CLEAR-COLOR ;draw-ball JSR2` ``` ``` ` ( update paddles )` ` ;update-paddles JSR2` ``` ``` ` ( update ball )` ` ;update-ball JSR2` ``` ``` ` ( draw paddles )` ` .left/x LDZ2 .left/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` ` .right/x LDZ2 .right/y LDZ2 PADDLE-COLOR ;draw-paddle JSR2` ``` ``` ` ( draw ball )` ` BALL-COLOR ;draw-ball JSR2` `BRK` ````` ``` ``` `## reset` ``` ``` ````` `@reset ( -- )` ` ( initialize ball )` ` .Screen/width DEI2 BALL-SIZE SUB2` ` HALF2 .ball/x STZ2` ` .Screen/height DEI2 BALL-SIZE SUB2` ` HALF2 .ball/y STZ2` `RTN` ````` ``` ``` `## ball-related` ``` ``` `### update-ball` ``` ``` ````` `@update-ball ( -- )` ` ( get speed-x and x )` ` .ball/speed-x LDZ2 .ball/x LDZ2 ( get x )` ` ADD2 ( add them together )` ` .ball/x STZ2 ( store new x )` ``` ``` ` ( get speed-y and y )` ` .ball/speed-y LDZ2 .ball/y LDZ2 ( get y )` ` ADD2 ( add them together )` ` .ball/y STZ2 ( store new y )` ``` ``` ` ( check collisions with walls )` ` &check-top-wall` ` .ball/y LDZ2` ` WALL-MARGIN` ` LTH2 ( is ball-y less than the margin? )` ` ,&set-positive-speed JCN` ` ,&check-bottom-wall JMP` ``` ``` ` &set-positive-speed` ` BALL-POSITIVE-SPEED .ball/speed-y STZ2` ` ,&continue JMP` ``` ``` ` &check-bottom-wall` ` .ball/y LDZ2 BALL-SIZE ADD2 ( y + ball size )` ` .Screen/height DEI2 WALL-MARGIN SUB2 ( height - margin )` ` GTH2` ` ,&set-negative-speed JCN` ` ,&continue JMP` ``` ``` ` &set-negative-speed` ` BALL-NEGATIVE-SPEED .ball/speed-y STZ2` ` &continue` ``` ``` ` ( check collisions with paddles )` ` &check-left-paddle` ` .ball/x LDZ2` ` MARGIN PADDLE-WIDTH ADD2` ` LTH2 ( is ball-x less than the margin + paddle-width? )` ` ,&x-in-left JCN` ` ,&check-right-paddle JMP` ``` ``` ` &x-in-left` ` .ball/y LDZ2 DUP2` ` .left/y LDZ2 BALL-SIZE SUB2 GTH2 ( first flag ) STH` ` .left/y LDZ2 PADDLE-HEIGHT ADD2 LTH2 ( second flag )` ` STHr ( retrieve first flag )` ` AND ( AND the two flags together )` ` ,&bounce-left JCN` ``` ``` ` .ball/x LDZ2 #0000 NEQ2 ( has it reached the wall ? )` ` ,&finish JCN` ``` ``` ` &reset-left` ` ( here you can add a point to the right paddle )` ` ;reset JSR2` ` ,&finish JMP` ``` ``` ` &bounce-left` ` BALL-POSITIVE-SPEED .ball/speed-x STZ2` ` ,&finish JMP` ``` ``` ` &check-right-paddle` ` .ball/x LDZ2 BALL-SIZE ADD2` ` .Screen/width DEI2 MARGIN SUB2 PADDLE-WIDTH SUB2` ` GTH2 ( is ball-x + ball-size greater than the screen width - margin - paddle-width? )` ` ,&x-in-right JCN` ` ,&finish JMP` ``` ``` ` &x-in-right` ` .ball/y LDZ2 DUP2` ` .right/y LDZ2 BALL-SIZE SUB2 GTH2 ( first flag ) STH` ` .right/y LDZ2 PADDLE-HEIGHT ADD2 LTH2 ( second flag )` ` STHr ( retrieve first flag )` ` AND ( AND the two flags together )` ` ,&bounce-right JCN` ``` ``` ` .ball/x LDZ2` ` .Screen/width DEI2 NEQ2 ( has it reached the wall ? )` ` ,&finish JCN` ``` ``` ` &reset-right` ` ( here you can add a point to the left paddle )` ` ;reset JSR2` ` ,&finish JMP` ``` ``` ` &bounce-right` ` BALL-NEGATIVE-SPEED .ball/speed-x STZ2` ` ,&finish JMP` ``` ``` ` &finish` `RTN` ````` ``` ``` `### draw-ball` ``` ``` ````` `@draw-ball ( color -- )` ` ( set initial x and y )` ` .ball/x LDZ2 .Screen/x DEO2` ` .ball/y LDZ2 .Screen/y DEO2` ``` ``` ``` ``` ` ( set sprite address )` ` ;ball-sprite .Screen/addr DEO2` ``` ``` ` ( auto byte: draw +1 sprite )` ` ( set auto addr and auto y )` ` #16 .Screen/auto DEO` ``` ``` ` ( set color from working stack: )` ` .Screen/sprite` ` ( draw two rows: )` ` DEOk DEO` `RTN` ````` ``` ``` `## paddle-related` ``` ``` `### update-paddles` ``` ``` ````` `@update-paddles ( -- )` ` &left` ` ( left paddle: up 10 and down 20 buttons )` ` .Controller/button DEI` ` DUP #10 AND ( check bit for up )` ` ,&left-up JCN` ` DUP #20 AND ( check bit for down )` ` ,&left-down JCN` ``` ``` ` ,&right JMP ( jump if neither of them were pressed )` ``` ``` ` &left-up` ` .left/y LDZ2 PADDLE-SPEED SUB2 .left/y STZ2` ` ,&right JMP` ` &left-down` ` .left/y LDZ2 PADDLE-SPEED ADD2 .left/y STZ2` ` ,&right JMP` ``` ``` ` &right` ` ( right paddle: ctrl/A 01 and alt/B 02 buttons )` ` DUP #01 AND ( check bit for A )` ` ,&right-up JCN` ` DUP #02 AND ( check bit for B )` ` ,&right-down JCN` ``` ``` ` ,&end JMP ( jump if neither of them were pressed )` ``` ``` ` &right-up` ` .right/y LDZ2 PADDLE-SPEED SUB2 .right/y STZ2` ` ,&end JMP` ` &right-down` ` .right/y LDZ2 PADDLE-SPEED ADD2 .right/y STZ2` ``` ``` ` &end` ` POP ( pop duplicate value of button )` `RTN` ````` ``` ``` `## draw-paddle` ``` ``` ````` `@draw-paddle ( x^ y^ color -- )` ` ( save color )` ` STH` ``` ``` ` ( set initial y and x )` ` .Screen/y DEO2` ` .Screen/x DEO2` ` ( set sprite address )` ` ;paddle-sprite .Screen/addr DEO2` ``` ``` ` ( auto byte: length: +1 sprite )` ` ( set auto addr and auto y )` ` #16 .Screen/auto DEO` ``` ``` ` ( get color from return stack: )` ` STHr .Screen/sprite` ` ( draw three rows: )` ` DEOk DEOk DEO` `RTN` ````` ``` ``` `## draw-background` ``` ``` ````` `@draw-background ( -- )` ` ;tile-background .Screen/addr DEO2 ( set tile address )` ``` ``` ` .Screen/height DEI2 WALL-MARGIN SUB2 ( set limit )` ` WALL-MARGIN ( set initial y )` ` &loop-y` ` DUP2 .Screen/y DEO2 ( set y coordinate )` ``` ``` ` ( draw row )` ` .Screen/width DEI2 #0000 ( set limit and initial x )` ` &loop-x` ` DUP2 .Screen/x DEO2 ( set x coordinate )` ` #03 .Screen/sprite DEO ( draw 1bpp sprite with color 3 and 0 )` ` #0008 ADD2 ( increment x )` ` GTH2k ( is the width greater than x? aka is x less than the width ? )` ` ,&loop-x JCN ( jump if x is less than the limit )` ` POP2 POP2 ( remove x and limit )` ``` ``` ` #0008 ADD2 ( increment y )` ` GTH2k ( is the limit greater than y? aka is y less than the limit ? )` ` ,&loop-y JCN ( jump if y is less than the limit )` ` POP2 POP2 ( remove y and limit )` `RTN` ````` ``` ``` `## data` ``` ``` ````` `@tile-background 1122 4488 1122 4488` ``` ``` `@paddle-sprite` ` &tile0 [ 3f 7f e7 c3 c3 c3 c3 c3 00 00 18 3c 3c 3c 3c 3c ]` ` &tile1 [ fc fe ff ff ff ff ff ff 00 00 00 00 00 00 06 06 ]` ` &tile2 [ c3 c3 c3 c3 e7 ff ff ff 3c 3c 3c 3c 18 00 00 00 ]` ` &tile3 [ ff ff ff ff ff ff ff ff 06 06 06 06 06 06 06 06 ]` ` &tile4 [ ff ff ff ff ff ff 7f 3f 00 00 00 00 00 00 00 00 ]` ` &tile5 [ ff ff ff ff ff ff fe fc 06 06 06 06 06 1e 3c 00 ]` ``` ``` `@ball-sprite` ` &tile0 [ 03 0f 1f 39 70 70 f9 ff 00 00 00 06 0f 0f 06 00 ]` ` &tile1 [ c0 f0 f8 fc fe fe ff ff 00 00 00 00 08 0c 06 06 ]` ` &tile2 [ ff ff 7f 7f 3f 1f 0f 03 00 00 00 00 18 0f 01 00 ]` ` &tile3 [ ff ff fe fe fc f8 f0 c0 06 06 0c 1c 38 f0 c0 00 ]` ````` ``` ``` `whew! :)` ``` ``` `# more possibilities` ``` ``` `here are some extra possibilities for you to practice and try to implement:` ``` ``` `* count and draw some sort of score` `* change color of the ball and/or the paddle when they are colliding` `* change the direction or type of bounce depending on the part of the paddle that hits the ball` `* start the game when a button is pressed` `* "random" initial direction of the ball` `* variable speed of the ball and/or paddles` `* etc!` ``` ``` `share what you create based on all this! :)` ``` ``` `# day 7` ``` ``` `in {uxn tutorial day 7} we talk about the devices in the varvara computer that we haven't covered yet: audio, file, and datetime.` ``` ``` `this should be a light and calm end of our journey, as it has to do less with programming logic and more with the input and output conventions in these devices.` ``` ``` `first, i invite you to take a break!` ``` ``` `then, keep exploring, and share your findings!` ``` ``` `# support` ``` ``` ```if you enjoyed this tutorial and found it helpful, consider sharing it and giving it your {support} :) ``` ``` ```