finished uxn tutorial day 6

This commit is contained in:
sejo 2021-09-14 17:05:16 -05:00
parent 5f83cf6e71
commit 9215140d60
2 changed files with 76 additions and 37 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -4,7 +4,9 @@
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, and for checking collisions.
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, and for checking collisions.
# general logic
@ -280,7 +282,11 @@ this should be the signature for our subroutine:
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.
also note that we will do that because 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: we would only need to adapt the vertical limit of the loop to correspond to the height of the screen, and that would be it!
we could then just jump to the section regarding the paddles. however, what follows can be interesting as a way of looking at possible approach to write more complex uxntal code :)
### using stack wrangling
@ -300,8 +306,10 @@ we could do the following:
```
ROT2 ( ws: x* width* height* y* )
DUP2 ( ws: x* width* height* y* y* )
( set initial y: )
.Screen/y DEO2 ( ws: x* width* height* y* )
( calculate and stash vertical limit )
ADD2 ( ws: x* width* limit-y* )
STH2 ( ws: x* width* / rs: limit-y* )
@ -323,8 +331,10 @@ in theory, the first part of our subroutine could look like:
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* )
@ -336,12 +346,13 @@ in theory, the first part of our subroutine could look like:
( 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. therefore, in the next iteration we wouldn't be able to use them again, as they would be lost.
the problem is that inside the loop, both STH2r instructions retrieve and consume the values for x and width from the return stack. therefore, in the next iteration we wouldn't be able to use them again, as they would be lost.
we can think we could replace these instructions with STH2kr:
@ -352,11 +363,11 @@ we can think we could replace these instructions with STH2kr:
STH2kr ( ws: x* / rs: limit-y* width* x* )
```
but then we can't retrieve the width because the x is still at the top!
but then we can't retrieve the width because the x is still at the top of the return stack!
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? maybe with a swap:
how can we put the width at the top of the return stack? maybe with a swap applied to the return stack:
```
SWP2r ( ws: x* / rs: limit-y* x* width* )
@ -383,7 +394,7 @@ 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:
but ah, before doing the comparison and jumping, we should rearrange the return stack so that it corresponds to the ordering we had at the beginning of the loop:
```
SWP2r ( ws: y* limit-y* / rs: x* limit-y* width* )
@ -411,8 +422,10 @@ after all this, our subroutine would look like the following:
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* )
@ -464,7 +477,14 @@ 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:
we'll declare the following labels for our variables, after the RTN that ends the subroutine:
```
( variables )
&height $2 &width $2 &y $2 &x $2 &limit-y $2
```
now, we start the subroutine in the same way as before, setting the address for our sprite:
```
( initial ws: x* y* width* height* addr* )
@ -484,7 +504,7 @@ 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:
we can then set the initial y and calculate the vertical limit, using the values stored in the variables:
```
( set initial y )
@ -604,13 +624,13 @@ we can think of the two paddles of the game as two rectangles, each one with its
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.
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
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.
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: if we reuse it, we'll end up drawing with the same color and in the same layer as the background texture!
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 :)
@ -689,7 +709,7 @@ or we could do it more traditionally:
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.
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. for this, and/or for the changes in coordinates, we can take advantage of the screen auto byte.
however, in this case i'll go for the first approach, and i'll manually set the address for each tile.
@ -763,7 +783,9 @@ it would be up to discussion if there are more efficient ways of drawing it. for
@draw-sprite ( x* y* width height addr* color )
```
it could work similar to the draw-tiles subroutine we created above. it could be a good exercise for you to try, but in this case i'll stay with the manual approach.
it could work similar to the draw-tiles subroutine we created above.
creating that could be a good exercise for you to try! in this case i'll just stay with the manual approach.
the good thing of this process being in a subroutine is that we can "forget" about its inner working and just use it :)
@ -780,7 +802,7 @@ let's reserve some space in the zero page for the x and y coordinates of each pa
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:
we can have a couple of macros to hold the dimensions of the paddles in order to use it later, and also its color:
```
%PADDLE-WIDTH { #0010 } ( 2 tiles )
@ -797,7 +819,7 @@ a margin to separate the paddles from the borders could be nice as well:
finally, let's bring back our HALF2 macro:
```
%HALF2 { #01 SFT2 }
%HALF2 { #01 SFT2 } ( short -- short/2 )
```
### initialize positions
@ -829,7 +851,7 @@ DUP2
### draw paddles
in order to draw each paddle, we could do the following inside our on-frame screen vector:
in order to draw each paddle, we can do the following procedure inside our on-frame screen vector:
```
( draw paddles )
@ -839,7 +861,7 @@ in order to draw each paddle, we could do the following inside our on-frame scre
### the program so far
omitting the definition of the subroutines, and just to clarify, right now our program would look like the following:
omitting the definition of the 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
@ -854,7 +876,7 @@ omitting the definition of the subroutines, and just to clarify, right now our p
( macros )
%RTN { JMP2r }
%HALF2 { #01 SFT2 }
%HALF2 { #01 SFT2 } ( short -- short/2 )
( constants )
%PADDLE-WIDTH { #0010 } ( 2 tiles )
@ -1065,6 +1087,7 @@ in our setup subroutine we can assign values to them, e.g. at the middle of the
.Screen/width DEI2 BALL-SIZE SUB2
HALF2
.ball/x STZ2
.Screen/height DEI2 BALL-SIZE SUB2
HALF2
.ball/y STZ2
@ -1168,6 +1191,7 @@ if, for example, we initialize speed-x with 1:
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 )
@ -1199,7 +1223,7 @@ in uxn, the result of adding ffff and 0001 is 0000: we would say we are overflow
let's look at it the other way around: if we have 1, and we add ffff, we get 0, that is 1 less than 1.
if we have 2, and we add ffff:
if we have 0002, and we add ffff:
```
0000 0000 0000 0010
@ -1208,7 +1232,7 @@ if we have 2, and we add ffff:
1 0000 0000 0000 0001
```
we get 1, that is 1 less than 2.
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.
@ -1236,6 +1260,7 @@ going back to our code, if we initialize our speed with
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 )
@ -1258,12 +1283,12 @@ based on what we just discussed, we can start our update-ball subroutine with th
```
@update-ball ( -- )
( get speed-x and x )
.ball/speed-x LDZ2 .ball/x LDZ2 ( get 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 ( get y )
.ball/speed-y LDZ2 .ball/y LDZ2
ADD2 ( add them together )
.ball/y STZ2 ( store new y )
RTN
@ -1303,7 +1328,7 @@ 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
or another one:
another one:
* check if the ball is hitting the top wall
* if it is, then set a positive speed
@ -1386,6 +1411,7 @@ our update-ball subroutine looks like the following right now:
&set-positive-speed
BALL-POSITIVE-SPEED .ball/speed-y STZ2
,&continue JMP
&check-bottom-wall
.ball/y LDZ2 BALL-SIZE ADD2 ( y + ball size )
@ -1400,7 +1426,7 @@ our update-ball subroutine looks like the following right now:
RTN
```
you can test it using different initial speed-y inside setup. the ball should be bouncing now! :)
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
@ -1496,15 +1522,17 @@ we can't really check if the x coordinate is less than 0, because as we discusse
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 LDZ2 BALL-SIZE ADD2 ( ball-x + ball-size )
.Screen/width DEI2 MARGIN SUB2 PADDLE-WIDTH SUB2
GTH2 ( is ball-x + ball-size greater than the screen width - margin - paddle-width? )
GTH2 ( is ball's right coordinate greater than the screen width - margin - paddle-width? )
,&x-in-right JCN
,&finish JMP
@ -1516,7 +1544,8 @@ for the right paddle we will do the same as above, but changing the comparisons
AND ( AND the two flags together )
,&bounce-right JCN
.ball/x LDZ2 .Screen/width DEI2 NEQ2 ( has it reached the wall ? )
.ball/x LDZ2
.Screen/width DEI2 NEQ2 ( has it reached the wall ? )
,&finish JCN
&reset-right
@ -1529,10 +1558,12 @@ for the right paddle we will do the same as above, but changing the comparisons
,&finish JMP
&finish
RTN
```
that should be it! let's define a reset subroutine so that we can actually assemble and run the game!
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
@ -1548,26 +1579,29 @@ here we'll just define a reset subroutine that returns the ball to the middle of
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 whatnot.
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 ]
|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 }
%HALF2 { #01 SFT2 } ( short -- short/2 )
%DOUBLE2 { #10 SFT2 }
( constants )
@ -1580,8 +1614,8 @@ here's all of the code we wrote today!
%BALL-POSITIVE-SPEED { #0001 }
%BALL-NEGATIVE-SPEED { #ffff }
%CLEAR-COLOR { #40 }
%MARGIN { #0010 }
%WALL-MARGIN { #0010 }
%MARGIN { #0010 } ( left and right )
%WALL-MARGIN { #0010 } ( top and bottom )
( zero page )
|0000
@ -1744,7 +1778,8 @@ RTN
AND ( AND the two flags together )
,&bounce-right JCN
.ball/x LDZ2 .Screen/width DEI2 NEQ2 ( has it reached the wall ? )
.ball/x LDZ2
.Screen/width DEI2 NEQ2 ( has it reached the wall ? )
,&finish JCN
&reset-right
@ -1988,7 +2023,7 @@ RTN
&tile3 [ ff ff fe fe fc f8 f0 c0 06 06 0c 1c 38 f0 c0 00 ]
```
whew!
# more possibilities
@ -1999,11 +2034,15 @@ here are some extra possibilities for you to practice and try to implement:
* 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 or game
* variable speed of the ball and/or paddles
* etc!
share what you create based on all this! :)
# coming soon: day 7
that's it for now!
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!