progress on uxn tutorial day 6
This commit is contained in:
parent
da300d3b52
commit
9b83c2cbb4
Binary file not shown.
After Width: | Height: | Size: 520 B |
Binary file not shown.
After Width: | Height: | Size: 486 B |
Binary file not shown.
After Width: | Height: | Size: 527 B |
Binary file not shown.
After Width: | Height: | Size: 892 B |
Binary file not shown.
After Width: | Height: | Size: 963 B |
|
@ -0,0 +1,916 @@
|
|||
# uxn tutorial: day 6, towards pong
|
||||
|
||||
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 snippets of code, we cover strategies for drawing and controlling multi-tile sprites, checking collisions, and drawing with a font.
|
||||
|
||||
# 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
|
||||
* drawing and counting of the score
|
||||
* clickable start button
|
||||
|
||||
# drawing the background: repeating a tile
|
||||
|
||||
in the previous section of the tutorial we discussed a way of creating a loop in order to repeat a 1bpp tile multiple times in a row.
|
||||
|
||||
here we will generalize that procedure into a draw-tiles subroutine that draws a rectangle filled with a given tile. it will receive the x,y coordinates of the top left corner of the rectangle, its width and height in pixels, and the address of the tile:
|
||||
|
||||
```
|
||||
@draw-tiles ( x* y* width* height* addr* -- )
|
||||
```
|
||||
|
||||
a reminder that we are using the convention of adding an asterisk (*) after the name of a value to indicate its a short.
|
||||
|
||||
we will detail how to get to two versions of this subroutine, one that relies on heavy stack wrangling, and other one that uses variables. this in order to compare both approaches and give us clues of the procedures that we will develop afterwards today.
|
||||
|
||||
## 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 ]
|
||||
|90 @Mouse [ &vector $2 &x $2 &y $2 &state $1 &wheel $1 ]
|
||||
|
||||
( macros )
|
||||
%RTN { JMP2r }
|
||||
|
||||
( main program )
|
||||
|0100
|
||||
( 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 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
|
||||
|
||||
### concrete version
|
||||
|
||||
before abstracting it away, i recommed we write it with concrete numbers.
|
||||
|
||||
let's say our initial x is 0008, our width is 0100, and the tile we are drawing is tile-background.
|
||||
|
||||
the limit, x+width, would be 0108.
|
||||
|
||||
the first step, drawing the tile in x would be:
|
||||
|
||||
```
|
||||
;tile-background .Screen/addr DEO2 ( set tile address )
|
||||
#0008 .Screen/x DEO2 ( set initial x )
|
||||
|
||||
#03 .Screen/sprite DEO ( draw sprite with color 3 and 0 )
|
||||
```
|
||||
|
||||
adding 8 to x we know already:
|
||||
|
||||
```
|
||||
.Screen/x DEI2 #0008 ADD2 ( add 8 to x )
|
||||
.Screen/x DEO2 ( store new x )
|
||||
```
|
||||
|
||||
and checking if x is less than the limit, jumping if it is, would be something like:
|
||||
|
||||
```
|
||||
.Screen/x DEI2 #0108 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 )
|
||||
#0008 .Screen/x DEO2 ( set initial x )
|
||||
|
||||
&loop-x
|
||||
#03 .Screen/sprite DEO ( draw sprite with color 3 and 0 )
|
||||
.Screen/x DEI2 #0008 ADD2 DUP2 ( add 8 to x )
|
||||
.Screen/x DEO2 ( store new x )
|
||||
#0108 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.
|
||||
|
||||
### abstracting
|
||||
|
||||
now, let's say we want to have the previous code work with any given initial x and width, present in the stack before starting.
|
||||
|
||||
we can even think of it as a subroutine by itself with the following signature:
|
||||
|
||||
```
|
||||
@draw-tiles-in-a-row ( x* width* -- )
|
||||
```
|
||||
|
||||
let's assume for the moment that the address of the sprite was already set, in order to focus in x and width.
|
||||
|
||||
when starting the subroutine, the width is at the top of the stack, followed by initial x.
|
||||
|
||||
we can use these two values to calculate the limit, that we can stash in the return stack.
|
||||
|
||||
one way of achieving that could be:
|
||||
|
||||
```
|
||||
OVR2 ( ws: x* width* x* )
|
||||
ADD2 ( ws: x* limit* )
|
||||
STH2 ( ws: x* / rs: limit* )
|
||||
```
|
||||
|
||||
another one:
|
||||
|
||||
```
|
||||
ADD2k ( ws: x* width* limit* )
|
||||
STH2 ( ws: x* width* / rs: limit* )
|
||||
POP2 ( ws: x* / rs: limit* )
|
||||
```
|
||||
|
||||
remember that we are showing the top of the stacks at their right.
|
||||
|
||||
after these steps, the initial x is at the top of the stack, so we can send it directly to the screen.
|
||||
|
||||
the last change that we would need is to replace our hardcoded limit with a STH2kr instruction (copy limit from the return stack into the working stack), and end our routine with a POP2r (remove limit from return stack).
|
||||
|
||||
our subroutine would then look as follows:
|
||||
|
||||
```
|
||||
@draw-tiles-in-a-row ( x* width* -- )
|
||||
OVR2 ( ws: x* width* x* )
|
||||
ADD2 ( ws: x* limit* )
|
||||
STH2 ( ws: x* / rs: limit* )
|
||||
.Screen/x DEO2 ( set initial x )
|
||||
|
||||
&loop-x
|
||||
#03 .Screen/sprite DEO ( draw sprite with color 3 and 0 )
|
||||
.Screen/x DEI2 #0008 ADD2 DUP2 ( add 8 to x )
|
||||
.Screen/x DEO2 ( store new x )
|
||||
STH2kr ( copy limit from rs into ws )
|
||||
LTH2 ,&loop-x JCN ( jump if x is less than the limit )
|
||||
POP2r ( pop limit from rs )
|
||||
RTN
|
||||
```
|
||||
|
||||
### 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 ]
|
||||
|90 @Mouse [ &vector $2 &x $2 &y $2 &state $1 &wheel $1 ]
|
||||
|
||||
( macros )
|
||||
%RTN { JMP2r }
|
||||
|
||||
( main program )
|
||||
|0100
|
||||
( set system colors )
|
||||
#2ce9 .System/r DEO2
|
||||
#01c0 .System/g DEO2
|
||||
#2ce5 .System/b DEO2
|
||||
|
||||
;tile-background .Screen/addr DEO2 ( set tile address )
|
||||
#0000 ( initial x )
|
||||
.Screen/width DEI2 ( get screen width )
|
||||
;draw-tiles-in-a-row JSR2
|
||||
BRK
|
||||
|
||||
@draw-tiles-in-a-row ( x* width* -- )
|
||||
OVR2 ( ws: x* limit* x* )
|
||||
ADD2 ( ws: x* limit* )
|
||||
STH2 ( ws: x* / rs: limit* )
|
||||
.Screen/x DEO2 ( set initial x )
|
||||
|
||||
&loop-x
|
||||
#03 .Screen/sprite DEO ( draw sprite with color 3 and 0 )
|
||||
.Screen/x DEI2 #0008 ADD2 DUP2 ( add 8 to x )
|
||||
.Screen/x DEO2 ( store new x )
|
||||
STH2kr ( copy limit from rs into ws )
|
||||
LTH2 ,&loop-x JCN ( jump if x is less than the limit )
|
||||
POP2r ( pop limit from rs )
|
||||
RTN
|
||||
|
||||
@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
|
||||
|
||||
### concrete version
|
||||
|
||||
let's use the same numbers as before, assuming that our initial y is 0008, our height is 0100, and therefore our limit would be 0108.
|
||||
|
||||
in the case of x, let's start at 0000 and have a width corresponding to the screen width.
|
||||
|
||||
as the address wouldn't change in the process, we can set it up at the top and forget about it.
|
||||
|
||||
the following code is based on the x loop, but it now draws a row in a given 'y' coordinate, adds 8 to it and then checks if it's less than the limit:
|
||||
|
||||
```
|
||||
;tile-background .Screen/addr DEO2 ( set tile address )
|
||||
|
||||
#0008 .Screen/y ( set initial y )
|
||||
|
||||
&loop-y
|
||||
( prepare and draw row )
|
||||
#0000 ( initial x )
|
||||
.Screen/width DEI2 ( get screen width )
|
||||
;draw-tiles-in-a-row JSR2
|
||||
|
||||
.Screen/y DEI2 #0008 ADD2 DUP2 ( add 8 to y )
|
||||
.Screen/y DEO2 ( store new y )
|
||||
#0108 ( put limit in top of the stack )
|
||||
LTH2 ,&loop-y JCN ( jump if x is less than the limit )
|
||||
```
|
||||
|
||||
### abstract version
|
||||
|
||||
now, before jumping right into emulating the solution for drawing the row, let's note that in this case it is not that easy.
|
||||
|
||||
why? because the idea for our draw-tiles subroutine is that it should be able to receive the initial x and the width of the rectangle, and right now these values are hardcoded inside the loop.
|
||||
|
||||
this should be the signature for our subroutine:
|
||||
|
||||
```
|
||||
@draw-tiles ( x* y* width* height* addr* -- )
|
||||
```
|
||||
|
||||
we can approach this problem either with some "stack wrangling", or with "variables".
|
||||
|
||||
also note that we are trying to get to a generalized subroutine; if we just wanted to cover all the screen with a sprite we have all the required code already.
|
||||
|
||||
### using stack wrangling
|
||||
|
||||
in principle we could just manipulate the items given in the stack, stashing them when appropriate, in order to adapt our subroutine to its signature.
|
||||
|
||||
first of all, the tile address is the value at the top of the stack. we can just consume it and forget about it:
|
||||
|
||||
```
|
||||
( initial ws: x* y* width* height* addr* )
|
||||
.Screen/addr DEO2 ( ws: x* y* width* height* )
|
||||
```
|
||||
|
||||
thinking about the vertical loop, we need to calculate its limit adding height to y, and we need to set the initial y.
|
||||
|
||||
we could do the following:
|
||||
|
||||
```
|
||||
ROT2 ( ws: x* width* height* y* )
|
||||
DUP2 ( ws: x* width* height* y* y* )
|
||||
.Screen/y DEO2 ( set initial y, ws: x* width* height* y* )
|
||||
ADD2 ( ws: x* width* limit-y* )
|
||||
STH2 ( ws: x* width* / rs: limit-y* )
|
||||
```
|
||||
|
||||
now, we might be able to stash also the width and 'x', as we need them afterwards in their original order (first x, then width )
|
||||
|
||||
```
|
||||
STH2 ( ws: x* / rs: limit-y* width* )
|
||||
STH2 ( ws: / rs: limit-y* width* x* )
|
||||
```
|
||||
|
||||
in theory, the first part of our subroutine could look like:
|
||||
|
||||
```
|
||||
@draw-tiles ( x* y* width* height* addr* -- )
|
||||
( set tile address )
|
||||
.Screen/addr DEO2 ( ws: x* y* width* height* )
|
||||
|
||||
ROT2 ( ws: x* width* height* y* )
|
||||
DUP2 ( ws: x* width* height* y* y* )
|
||||
( set initial y )
|
||||
.Screen/y DEO2 ( set initial y, ws: x* width* height* y* )
|
||||
( calculate and stash limit-y )
|
||||
ADD2 ( ws: x* width* limit-y* )
|
||||
STH2 ( ws: x* width* / rs: limit-y* )
|
||||
|
||||
( stash width and x )
|
||||
STH2 STH2 ( ws: / rs: limit-y* width* x* )
|
||||
|
||||
&loop-y
|
||||
( prepare and draw row )
|
||||
( retrieve x )
|
||||
STH2r ( ws: x* / rs: limit-y* width* )
|
||||
( retrieve width )
|
||||
STH2r ( ws: x* width* / rs: limit-y* )
|
||||
;draw-tiles-in-a-row JSR2
|
||||
```
|
||||
|
||||
the problem is that inside the loop, both STH2r instructions retrieve and consume from the return stack the values for x and width.
|
||||
|
||||
we can think we could replace them with STH2kr:
|
||||
|
||||
```
|
||||
&loop-y
|
||||
( prepare and draw row )
|
||||
( retrieve x )
|
||||
STH2kr ( ws: x* / rs: limit-y* width* x* )
|
||||
```
|
||||
|
||||
but then we can't retrieve the width because the x is still at the top!
|
||||
|
||||
oh, many difficulties, but for the sake of the stack wrangling example, let's continue solving this (?)
|
||||
|
||||
how can we put the width at the top of the return stack?
|
||||
|
||||
```
|
||||
SWP2r ( ws: x* / rs: limit-y* x* width* )
|
||||
```
|
||||
|
||||
then we can then retrieve it and use it:
|
||||
|
||||
```
|
||||
STH2kr ( ws: x* width* / rs: limit-y* x* width* )
|
||||
;draw-tiles-in-a-row JSR2 ( ws: / rs: limit-y* x* width* )
|
||||
```
|
||||
|
||||
what's next? add 8 to y, and check if it's less than the limit. the first part goes without problems:
|
||||
|
||||
```
|
||||
.Screen/y DEI2 #0008 ADD2 DUP2 ( add 8 to y; ws: y* y* / rs: limit-y* x* width* )
|
||||
.Screen/y DEO2 ( store new y; ws: y* / rs: limit-y* x* width* )
|
||||
```
|
||||
|
||||
in order to get the limit into the working stack for the comparison, we have to rotate the return stack:
|
||||
|
||||
```
|
||||
ROT2r ( ws: y* / rs: x* width* limit-y* )
|
||||
STH2kr ( ws: y* limit-y* / rs: x* width* limit-y* )
|
||||
```
|
||||
|
||||
but ah, before doing the comparison and jumping, we should rearrange the return stack so that it corresponds to the order we had at the beginning of the loop:
|
||||
|
||||
```
|
||||
SWP2r ( ws: y* limit-y* / rs: x* limit-y* width* )
|
||||
ROT2r ( ws: y* limit-y* / rs: limit-y* width* x* )
|
||||
```
|
||||
|
||||
now we can do the comparison and jump:
|
||||
|
||||
```
|
||||
LTH2 ,&loop-y JCN ( jump if x is less than the limit )
|
||||
```
|
||||
|
||||
afterwards we should clear the return stack:
|
||||
|
||||
```
|
||||
POP2r POP2r POP2r
|
||||
```
|
||||
|
||||
after all this, our subroutine would look like the following:
|
||||
|
||||
```
|
||||
@draw-tiles ( x* y* width* height* addr* -- )
|
||||
( set tile address )
|
||||
.Screen/addr DEO2 ( ws: x* y* width* height* )
|
||||
|
||||
ROT2 ( ws: x* width* height* y* )
|
||||
DUP2 ( ws: x* width* height* y* y* )
|
||||
( set initial y )
|
||||
.Screen/y DEO2 ( set initial y, ws: x* width* height* y* )
|
||||
( calculate and stash limit-y )
|
||||
ADD2 ( ws: x* width* limit-y* )
|
||||
STH2 ( ws: x* width* / rs: limit-y* )
|
||||
|
||||
( stash width and x )
|
||||
STH2 STH2 ( ws: / rs: limit-y* width* x* )
|
||||
|
||||
&loop-y
|
||||
( prepare and draw row )
|
||||
( retrieve x )
|
||||
STH2kr ( ws: x* / rs: limit-y* width* x* )
|
||||
|
||||
( retrieve width )
|
||||
SWP2r ( ws: x* / rs: limit-y* x* width* )
|
||||
STH2kr ( ws: x* width* / rs: limit-y* x* width* )
|
||||
;draw-tiles-in-a-row JSR2 ( ws: / rs: limit-y* x* width* )
|
||||
|
||||
.Screen/y DEI2 #0008 ADD2 DUP2 ( add 8 to y )
|
||||
.Screen/y DEO2 ( store new y )
|
||||
|
||||
( retrieve limit-y )
|
||||
ROT2r ( ws: y* / rs: x* width* limit-y* )
|
||||
STH2kr ( ws: y* limit-y* / rs: x* width* limit-y* )
|
||||
|
||||
( rearrange return stack )
|
||||
SWP2r ( ws: y* limit-y* / rs: x* limit-y* width* )
|
||||
ROT2r ( ws: y* limit-y* / rs: limit-y* width* x* )
|
||||
|
||||
LTH2 ,&loop-y JCN ( jump if x is less than the limit )
|
||||
|
||||
POP2r POP2r POP2r ( clear return stack )
|
||||
RTN
|
||||
```
|
||||
|
||||
we can then call it like the following in order to get a 256x256 square filled with tiles:
|
||||
|
||||
```
|
||||
#0008 #0008 ( x and y )
|
||||
#0100 #0100 ( width and height )
|
||||
;tile-background
|
||||
;draw-tiles JSR2
|
||||
```
|
||||
|
||||
=> ./img/screenshot_uxn-background-square.png screenshot showing a big square in the varvara screen composed of diagonal lines
|
||||
|
||||
### using variables
|
||||
|
||||
let's compare the previous approach with the use of relative variables.
|
||||
|
||||
we will go "all in" in a relatively wasteful way, without optimizing for procedures that could benefit from stack manipulation.
|
||||
|
||||
we start in the same way as before, setting the address for our sprite:
|
||||
|
||||
```
|
||||
( initial ws: x* y* width* height* addr* )
|
||||
.Screen/addr DEO2 ( ws: x* y* width* height* )
|
||||
```
|
||||
|
||||
then, we just store the next values in relative addresses:
|
||||
|
||||
```
|
||||
,&height STR2
|
||||
,&width STR2
|
||||
,&y STR2
|
||||
,&x STR2
|
||||
```
|
||||
|
||||
note that we go in reverse order. after these operations the stacks are empty.
|
||||
|
||||
we can then set the initial y and calculate the vertical limit:
|
||||
|
||||
```
|
||||
( set initial y )
|
||||
,&y LDR2 DUP2 ( ws: y* y* )
|
||||
.Screen/y DEO2 ( ws: y* )
|
||||
( calculate limit-y )
|
||||
,&height LDR2 ( ws: y* height* )
|
||||
ADD2 ( ws: limit-y* )
|
||||
,&limit-y STR2 ( ws: )
|
||||
```
|
||||
|
||||
our loop now would look as follows:
|
||||
|
||||
```
|
||||
&loop-y
|
||||
( retrieve x and width )
|
||||
,&x LDR2
|
||||
,&width LDR2
|
||||
( draw row )
|
||||
;draw-tiles-in-a-row JSR2
|
||||
|
||||
.Screen/y DEI2 #0008 ADD2 DUP2 ( add 8 to y )
|
||||
.Screen/y DEO2 ( store new y )
|
||||
|
||||
( retrieve vertical limit )
|
||||
,&limit-y LDR2
|
||||
|
||||
LTH2 ,&loop-y JCN ( jump if x is less than the limit )
|
||||
```
|
||||
|
||||
and that's it!
|
||||
|
||||
compare this with the "concrete" version we developed above, it's very similar!
|
||||
|
||||
the complete subroutine would look like the following:
|
||||
|
||||
```
|
||||
@draw-tiles ( x* y* width* height* addr* -- )
|
||||
( set tile address )
|
||||
.Screen/addr DEO2 ( ws: x* y* width* height* )
|
||||
|
||||
( store values )
|
||||
,&height STR2
|
||||
,&width STR2
|
||||
,&y STR2
|
||||
,&x STR2
|
||||
|
||||
( set initial y )
|
||||
,&y LDR2 DUP2 ( ws: y* y* )
|
||||
.Screen/y DEO2 ( ws: y* )
|
||||
|
||||
( calculate vertical limit )
|
||||
,&height LDR2 ( ws: y* height* )
|
||||
ADD2 ( ws: limit-y* )
|
||||
,&limit-y STR2 ( ws: )
|
||||
|
||||
&loop-y
|
||||
( retrieve x and width )
|
||||
,&x LDR2
|
||||
,&width LDR2
|
||||
( draw row )
|
||||
;draw-tiles-in-a-row JSR2
|
||||
|
||||
.Screen/y DEI2 #0008 ADD2 DUP2 ( add 8 to y )
|
||||
.Screen/y DEO2 ( store new y )
|
||||
|
||||
( retrieve vertical limit )
|
||||
,&limit-y LDR2
|
||||
|
||||
LTH2 ,&loop-y JCN ( jump if x is less than the limit )
|
||||
|
||||
RTN
|
||||
( variables )
|
||||
&height $2 &width $2 &y $2 &x $2 &limit-y $2
|
||||
```
|
||||
|
||||
as i said before, we can find here some opportunities for optimization.
|
||||
|
||||
maybe the vertical limit can be stashed in the return stack like in the draw-tiles-in-a-row loop, or maybe the variable for the height and for the initial y are not needed.
|
||||
|
||||
i'll let you figure them out :)
|
||||
|
||||
note that this subroutine as-is, requires 24 bytes of program memory more than the stack wrangling version.
|
||||
|
||||
in our case that's not much of a problem, but it's a good way of evaluating our priorities: super readable but probably inefficient code, super optimized but probably unreadable code (write-only code, they say), or something in the middle.
|
||||
|
||||
## draw-background
|
||||
|
||||
now that we have these nice subroutines, we can just wrap them in another one that will cover the whole background with our chosen tile.
|
||||
|
||||
for example:
|
||||
|
||||
```
|
||||
@draw-background ( -- )
|
||||
#0000 #0000 ( initial x and y )
|
||||
.Screen/width DEI2
|
||||
.Screen/height DEI2
|
||||
;tile-background
|
||||
;draw-tiles JSR2
|
||||
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.
|
||||
|
||||
nice! i recommend you take a little break; this was heavy!
|
||||
|
||||
# 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 the y coordinate with the controller.
|
||||
|
||||
## drawing the multi-tile paddles
|
||||
|
||||
in theory, we could use our previous draw-tiles subroutine to draw the rectangles corresponding to our paddles, with a given tile.
|
||||
|
||||
in practice, that might be a little bit inconvenient, because the sprite byte that determines the layer and color of our tile was left hardcoded inside our subroutine.
|
||||
|
||||
i leave it as an exercise for you to modify the subroutine so that the sprite byte is received as an argument in the stack :)
|
||||
|
||||
in any case, 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 81, but i will instead use 85 to have color 0 being transparent.
|
||||
|
||||
### 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 argument the x and y position of its top left corner:
|
||||
|
||||
```
|
||||
@draw-paddle ( x* y* -- )
|
||||
```
|
||||
|
||||
but let's add the color byte as well:
|
||||
|
||||
```
|
||||
@draw-paddle ( x* y* color -- )
|
||||
```
|
||||
|
||||
on one hand this 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.
|
||||
|
||||
there are many ways to do it, depending on taste.
|
||||
|
||||
we could for example draw the tiles in the following order, with the following operations:
|
||||
|
||||
* draw tile 0, then add 8 to x
|
||||
* draw tile 1, then add 8 to y
|
||||
* draw tile 3, then subtract 8 to x
|
||||
* draw tile 2, then add 8 to y
|
||||
* draw tile 4, then add 8 to x
|
||||
* draw tile 5
|
||||
|
||||
or we could do it more traditionally:
|
||||
|
||||
* 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
|
||||
|
||||
instead of subtracting we could recover x from the return stack, or from a relative variable.
|
||||
|
||||
a possible advantage of going in order is that we can increment the address of the sprite by 10 (16 in decimal) to get to the address of the next tile.
|
||||
|
||||
however, in this case i'll go for the first approach, and i'll manually set the address for each tile.
|
||||
|
||||
additionally, i'll save the color in the return stack:
|
||||
|
||||
```
|
||||
@draw-paddle ( x* y* color -- )
|
||||
( save color )
|
||||
STH
|
||||
|
||||
( set initial y and x )
|
||||
.Screen/y DEO2
|
||||
.Screen/x DEO2
|
||||
|
||||
( draw tile 0 )
|
||||
;paddle/tile0 .Screen/addr DEO2
|
||||
( copy color from return stack: )
|
||||
STHkr .Screen/sprite DEO
|
||||
|
||||
( add 8 to x: )
|
||||
.Screen/x DEI2 #0008 ADD2 .Screen/x DEO2
|
||||
|
||||
( draw tile 1 )
|
||||
;paddle/tile1 .Screen/addr DEO2
|
||||
STHkr .Screen/sprite DEO
|
||||
|
||||
( add 8 to y: )
|
||||
.Screen/y DEI2 #0008 ADD2 .Screen/y DEO2
|
||||
|
||||
( draw tile 3 )
|
||||
;paddle/tile3 .Screen/addr DEO2
|
||||
STHkr .Screen/sprite DEO
|
||||
|
||||
( sub 8 to x: )
|
||||
.Screen/x DEI2 #0008 SUB2 .Screen/x DEO2
|
||||
|
||||
( draw tile 2 )
|
||||
;paddle/tile2 .Screen/addr DEO2
|
||||
STHkr .Screen/sprite DEO
|
||||
|
||||
( add 8 to y: )
|
||||
.Screen/y DEI2 #0008 ADD2 .Screen/y DEO2
|
||||
|
||||
( draw tile 4 )
|
||||
;paddle/tile4 .Screen/addr DEO2
|
||||
STHkr .Screen/sprite DEO
|
||||
|
||||
( add 8 to x: )
|
||||
.Screen/x DEI2 #0008 ADD2 .Screen/x DEO2
|
||||
|
||||
( draw tile 5 )
|
||||
;paddle/tile5 .Screen/addr DEO2
|
||||
STHkr .Screen/sprite DEO
|
||||
|
||||
( clear return stack )
|
||||
POPr
|
||||
RTN
|
||||
```
|
||||
|
||||
that's it!
|
||||
|
||||
now 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
|
||||
|
||||
it would be up to discussion if there are more efficient ways of drawing it.
|
||||
|
||||
the good thing of this process being in a subroutine is that we can "forget" about its inner working and just use it :)
|
||||
|
||||
## 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 size of the paddles in order to use it later, and also its color:
|
||||
|
||||
```
|
||||
%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:
|
||||
|
||||
```
|
||||
%HALF2 { #01 SFT2 }
|
||||
```
|
||||
|
||||
### initialize positions
|
||||
|
||||
we can then initialize their positions.
|
||||
|
||||
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 could do the following inside our 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 subroutines, and just to clarify, 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 ]
|
||||
|90 @Mouse [ &vector $2 &x $2 &y $2 &state $1 &wheel $1 ]
|
||||
|
||||
( macros )
|
||||
%RTN { JMP2r }
|
||||
%HALF2 { #01 SFT2 }
|
||||
|
||||
( constants )
|
||||
%PADDLE-WIDTH { #0010 } ( 2 tiles )
|
||||
%PADDLE-HEIGHT { #0018 } ( 3 tiles )
|
||||
%PADDLE-COLOR { #c5 }
|
||||
%MARGIN { #0010 }
|
||||
|
||||
( zero page )
|
||||
|0000
|
||||
@left [ &x $2 &y $2 ]
|
||||
@right [ &x $2 &y $2 ]
|
||||
|
||||
( main program )
|
||||
|0100
|
||||
( 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
|
||||
|
||||
# the ball
|
||||
|
||||
## drawing the ball
|
||||
|
||||
## ball movement and collisions
|
||||
|
||||
# the score
|
||||
|
||||
## counting
|
||||
|
||||
## drawing
|
||||
|
||||
# start button
|
||||
|
||||
## drawing the start button
|
||||
|
||||
## checking for mouse position
|
||||
|
||||
|
||||
# coming soon: day 7
|
||||
|
||||
the following day of the {uxn tutorial} will consist in discussing all the remaining devices in the varvara computer.
|
||||
|
||||
meanwhile, i invite you to keep exploring, to share your findings, and to also take a break!
|
||||
|
||||
stay tuned!
|
||||
|
||||
# support
|
||||
|
||||
if you found this tutorial to be helpful, consider sharing it and giving it your {support} :)
|
Loading…
Reference in New Issue