💾 Archived View for compudanzas.net › uxn_tutorial_day_4.gmi captured on 2022-06-11 at 20:44:00. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-06-03)
-=-=-=-=-=-=-
en español:
this is the fourth section of the uxn tutorial!
here we discuss the animation loop of the varvara computer, via its screen device vector.
we also talk about using the program memory as a space for data using "variables". this allows us to save and retrieve data during the runtime of our programs, and can save us from complex stack wrangling :)
we discussed the varvara screen device on day 2, but we skipped its vector port in order to focus in how to draw with it:
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1 ]
now that we have the concept of vectors of devices on uxn tutorial day 3, let's jump right in into how to use the one in the screen!
the following line of uxntal would assign the absolute address of the label on-frame to the screen vector:
;on-frame .Screen/vector DEO2
uxn will jump to the location of at label at a rate of 60 times per second: we can use the subroutine under on-frame to change the contents of the screen, generating animation, and/or we can also use it for other timing-related purposes.
the following program demonstrates a basic but powerful use of the screen vector: on each frame, it draws a pixel at the given screen x,y coordinates, and it adds 1 to the value of the x coordinate:
( hello-line.tal ) ( devices ) |00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ] |20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1 ] ( init ) |0100 ( set system colors ) #2ce9 .System/r DEO2 #01c0 .System/g DEO2 #2ce5 .System/b DEO2 ( set initial x,y coordinates ) #0008 .Screen/x DEO2 #0008 .Screen/y DEO2 ( set screen vector ) ;on-frame .Screen/vector DEO2 BRK @on-frame ( -> ) ( draw a pixel in the bg with color 1 ) #01 .Screen/pixel DEO ( increment Screen/x ) .Screen/x DEI2 INC2 .Screen/x DEO2 BRK
note that the code is very similar to the one we wrote on day 2 for drawing a line.
in that one, we manually incremented the value of Screen/x in order to draw 6 pixels.
here, the code for incrementing Screen/x is called inside the on-frame subroutine instead, having it occurring 60 times per second.
these are some changes for you to try and practice:
the varvara screen vector opens up a whole world of possibilities!
it's worth noting that many of these possibilities require ways of storing and retrieving data between frames.
in the previous example, we are using the screen ports for the x and y coordinates as a way of storing the coordinates for the pixel.
but what happens when we want to draw different objects, each with its own set of coordinates and other characteristics that can change over time?
we can use labels in program memory in order to achieve that!
in a way, when storing our sprite data we have been doing something like that already.
we have labelled a section of memory with contents that are not instructions for uxn, but data; e.g.:
@square ff81 8181 8181 81ff
however, we have not used the data directly; we have sent its address to the corresponding screen device port.
we could use a similar system for storing e.g. x and y coordinates instead of sprite data:
@pixel-x 0008 @pixel-y 0008
or if we didn't want to initialize them here, we could define them as follows:
@pixel-x $2 @pixel-y $2
remember that $2 creates a relative pad of two byes: this makes pixel-y a label for an address in memory two bytes after pixel-x. and whatever code afterwards will happen two bytes after pixel-y.
we could also use labels and sublabels, in a manner that would be very similar to how we define devices and their ports:
@pixel [ &x $2 &y $2 ]
how could we read (load) and write (store) the contents of the memory at those labels?
here are the two instructions that would help us:
as we have discussed already, an absolute address will always be two bytes long.
in the short mode, LDA2 will load a short from the given address, and STA2 will store a short into the given address.
as an example, the following code would read the two bytes from pixel/x, increment them by one, and store them back into pixel/x:
;pixel/x LDA2 ( load pixel/x into the stack ) INC2 ( increment ) ;pixel/x STA2 ( store the result into pixel/x ) BRK @pixel [ &x $2 &y $2 ]
note the use of BRK before the pixel label in order to make uxn stop before reading the data as instructions.
the following is a variation that also duplicates the new pixel/x value in order to send it to Screen/x:
;pixel/x LDA2 ( load pixel/x into the stack ) INC2 ( increment ) DUP2 ( duplicate result ) .Screen/x DEO2 ( set as screen/x ) ;pixel/x STA2 ( and store the result into pixel/x ) BRK @pixel [ &x $2 &y $2 ]
note that we could have achieved the same result by storing the result, and then re-loading it and sending it as an output.
here we can see how a DUP2 can make that operation easier, as long as we keep a mental (or tangible!) model of what is happening on the stack.
a possible advantage of using absolute addresses is that we can initialize the contents of our variables at assembly time, e.g.:
@pixel [ &x 0008 &y 0008 ]
these initial contents will change whenever we use a STA instruction there :)
variables with absolute address work well for cases where we want to be able to access their contents from any part of our program (i.e. "global variables").
however, uxn has a better mechanism for those cases: the zero page!
as you may recall, the zero page consists of the first 256 addresses of program memory. normally, a program starts at address 0100, that is the next address after the zero page.
we can refer to any of the 256 addresses of the zero page using one byte only, instead of the two bytes that are needed for absolute addresses.
something important to keep in mind is that the contents of the zero page are not present in uxn roms.
this means that a caveat of using variables there, is that in order to initialize them we need to do it during runtime, by storing values from the stack into them.
labels for the zero page would work the same as before; we only need to specify that they are in the zero page with an absolute pad:
|0000 ( zero page ) @pixel [ &x $2 &y $2 ]
in order to refer to them, we would use the dot (.) rune for literal zero page addresses, instead of the colon (;) rune for literal absolute addresses.
the instructions for loading (reading) and storing (writing) from and to the zero page are:
in these instructions, the address will always be one byte long.
in the short mode, LDZ2 will load a short from the given address, and STZ2 will store a short into the given address.
the following example consists in the same line that grows, but now using the zero page to store the x and y coordinates of the pixel instead of the screen x and y ports.
in this case the program is longer, but it could be seen as a nice template for having other lines behaving in different ways:
( hello-line.tal ) ( devices ) |00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ] |20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1 ] ( zero page ) |0000 @pixel [ &x $2 &y $2 ] ( init ) |0100 ( set system colors ) #2ce9 .System/r DEO2 #01c0 .System/g DEO2 #2ce5 .System/b DEO2 ( set initial x,y coordinates ) #0008 .pixel/x STZ2 #0008 .pixel/y STZ2 ( set screen vector ) ;on-frame .Screen/vector DEO2 BRK @on-frame ( -> ) ( load x,y coordinates from zero page and send to screen ) .pixel/x LDZ2 .Screen/x DEO2 .pixel/y LDZ2 .Screen/y DEO2 ( draw a pixel in the bg with color 1 ) #01 .Screen/pixel DEO ( increment pixel/x ) .pixel/x LDZ2 INC2 .pixel/x STZ2 BRK
note the use of the literal zero page address rune (.) to refer to the .pixel label.
also, note that in the case of .pixel the address is referring to the zero page, accessed with LDZ/STZ, and in the case of .Screen the address is referring to the i/o address space, accessed with DEI/DEO.
note that the following instructions would also increment .pixel/x, but setting its address only once:
.pixel/x DUP LDZ2 INC2 ROT STZ2
i recommend you to follow how the stack contents change in each of the steps: keep in mind that some of the instructions are in short mode :)
this line of code contains the same amount of bytes than the previous one.
a possible disadvantage is that it might be less readable. but a possible advantage is that it could get converted into a macro:
( increment a short from the zero-page ) %ZP-INC2 { DUP LDZ2 INC2 ROT STZ2 } ( zp-address -- )
another possibility that we have in uxn that might be more appropriate for "local variables", consists in using relative addresses.
similar to variables in the zero page, in order to address these variables we only need one byte.
however, as these addresses are given as relative and signed offsets, they can only be reached if they are within the 256 bytes surrounding the instruction that loads or stores them.
the instructions for working in this way are:
similar to LDZ and STZ, in these instructions the address will always be one byte long.
in the short mode, LDR2 will load a short from the given address, and STR2 will store a short into the given address.
the following is the on-frame subroutine that draws the growing line, but storing the pixel coordinates in a "local" variable accessed via LDR and STR.
@on-frame ( -> ) ( load x,y coordinates from zero page and send to screen ) ,pixel/x LDR2 .Screen/x DEO2 ,pixel/y LDR2 .Screen/y DEO2 ( draw a pixel in the bg with color 1 ) #01 .Screen/pixel DEO ( increment pixel/x ) ,pixel/x LDR2 INC2 ,pixel/x STR2 BRK @pixel [ &x $2 &y $2 ]
note the use of the comma (,) rune to indicate that it's a relative address; uxnasm calculates the required offset assuming it will be used in the next instruction.
in this case we can't really duplicate that offset as we did previously with the zero-page address, because it is specific to the position in the code it was written.
if we declared these variables as sublabels of on-frame, the code would look as follows:
@on-frame ( -> ) ( load x,y coordinates from zero page and send to screen ) ,&pixel-x LDR2 .Screen/x DEO2 ,&pixel-y LDR2 .Screen/y DEO2 ( draw a pixel in the bg with color 1 ) #01 .Screen/pixel DEO ( increment pixel/x ) ,&pixel-x LDR2 INC2 ,&pixel-x STR2 BRK ( on-frame local variables ) &pixel-x $2 &pixel-y $2
note that in this case, the comma (,) rune is accompanied by the sublabel (&) rune.
the use of this kind of variables will make more sense in day 5 of the tutorial :)
the use of "variables" will help us now in discussing three different ways of animating a sprite:
we will review them separately in order to keep the examples relatively simple and readable.
note that these examples also serve as a way of discussing more uxntal programming possibilities, and they might be a litle bit overwhelming.
i recommend you to review and experiment with one at a time, patiently :)
we discussed already how to have uxn change the position of a pixel in the screen, leaving a trail.
changing that program in order to draw an 8x8 sprite instead would be relatively straightforward, and you may have tried it already: we would need to use Screen/sprite instead of Screen/pixel, with an appropriate byte to define its color and orientation, and we would need to set the address of our sprite data in Screen/addr.
that would result in a sprite that moves and that also leaves a trail: i invite you to try it first!
ok, that can be useful in some instances, but how can we avoid leaving the trail?
a possible way of achieving it would be by following this order of operations inside the on-frame subroutine:
this allows us to clear the sprite from its position in the previous frame, update its coordinates to a new position, and then draw it there.
the following program illustrates the previous points, having our square from day 2 travel from left to right in the middle of our screen.
it combines several things that we have covered in the past few days!
( hello-animated-sprite.tal ) ( devices ) |00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ] |20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1 ] ( macros/constants ) %HALF2 { #01 SFT2 } ( shift one bit to the right ) ( short -- short/2 ) %color-clear { #40 } ( clear 1bpp sprite from fg ) %color-2 { #4a } ( draw 1bpp sprite with color 2 and transparency ) ( zero page ) |0000 @sprite [ &pos-x $2 &pos-y $2 ] ( init ) |0100 ( set system colors ) #2ce9 .System/r DEO2 #01c0 .System/g DEO2 #2ce5 .System/b DEO2 ( set screen/y to half of screen minus 4 ) .Screen/height DEI2 HALF2 #0004 SUB2 .Screen/y DEO2 ( set sprite address ) ;square .Screen/addr DEO2 ( set screen vector ) ;on-frame .Screen/vector DEO2 BRK @on-frame ( -> ) ( 1: clear sprite ) ( clear sprite from the fg ) color-clear .Screen/sprite DEO ( 2: change position ) ( increment sprite/pos-x ) .sprite/pos-x LDZ2 INC2 .sprite/pos-x STZ2 ( 3 : draw sprite ) ( load x coordinate from zero page and send to screen ) .sprite/pos-x LDZ2 .Screen/x DEO2 ( draw sprite in fg with color 2 and transparency ) color-2 .Screen/sprite DEO BRK ( sprite data ) @square ff81 8181 8181 81ff
nice, isn't it? :)
as this is just an example to illustrate a point, there are some things that could be optimized in order to make our program smaller, and there are some things that could be useful but were left out. for example, there's not an initial value for the x coordinate, or the y coordinate is not used.
regarding optimization, and as an example, section 2 and the first part of section 3 of on-frame could have been written as follows:
( 2: change position ) ( increment sprite/pos-x ) .sprite/pos-x LDZ2 INC2 DUP2 ( duplicate result ) .sprite/pos-x STZ2 ( store the first copy of the result ) ( 3 : draw sprite ) ( use x coordinate from the stack and send to screen ) .Screen/x DEO2
as always, it is up to us how we want to navigate between shorter code and readability :)
here are some questions for you to ponder and try:
when we use the controller vector, we are acting based on a change in the button(s) or key(s) that were pressed or released. this can be very useful for some applications.
but, how can we deal with doing a continuous action when a key is kept pressed?
in some operating systems, if we keep a key pressed, it fires the controller vector several times, but not necessarily at the same rate as the screen vector!
this repetition might not allow for a smooth movement like what we can get if we check for the state of the controller inside the on-frame subroutine!
the following program allows us to control the horizontal position of our square using the arrow keys.
animated gif showing a square moving horizontally in the screen, apparently controlled by a human.
note the similarities between the previous program, and what we covered on uxn tutorial day 3!
( hello-moving-sprite.tal ) ( devices ) |00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ] |20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1 ] |80 @Controller [ &vector $2 &button $1 &key $1 ] ( macros/constants ) %HALF2 { #01 SFT2 } ( shift one bit to the right ) ( short -- short/2 ) %color-clear { #40 } ( clear 1bpp sprite from fg ) %color-2 { #4a } ( draw 1bpp sprite with color 2 and transparency ) ( zero page ) |0000 @sprite [ &pos-x $2 &pos-y $2 ] ( init ) |0100 ( set system colors ) #2ce9 .System/r DEO2 #01c0 .System/g DEO2 #2ce5 .System/b DEO2 ( set screen/y to half of screen minus 4 ) .Screen/height DEI2 HALF2 #0004 SUB2 .Screen/y DEO2 ( set sprite address ) ;square .Screen/addr DEO2 ( set screen vector ) ;on-frame .Screen/vector DEO2 BRK @on-frame ( -> ) ( 1: clear sprite ) ( clear sprite from the fg ) color-clear .Screen/sprite DEO ( 2: change position with arrow keys ) &check-arrows .Controller/button DEI #40 AND ( isolate bit 6, corresponding to Left ) ,&left JCN ( jump if not 0 ) .Controller/button DEI #80 AND ( isolate bit 7, corresponding to Right ) ,&right JCN ( jump if not 0 ) ( if none of those keys were pressed, draw without changes ) ,&draw JMP &left ( decrement sprite/pos-x ) .sprite/pos-x LDZ2 #0001 SUB2 .sprite/pos-x STZ2 ,&draw JMP &right ( increment sprite/pos-x ) .sprite/pos-x LDZ2 INC2 .sprite/pos-x STZ2 ( 3 : draw sprite ) &draw ( load x coordinate from zero page and send to screen ) .sprite/pos-x LDZ2 .Screen/x DEO2 ( draw sprite in fg with color 2 and transparency ) color-2 .Screen/sprite DEO BRK ( sprite data ) @square ff81 8181 8181 81ff
i invite you to adapt the code so that you can control the sprite in the four cardinal directions!
as you may have noticed, these two previous programs allow our sprite to go outside the screen.
if we wanted to avoid that, one way we could do it would be by adding (more) conditionals.
for example, instead of having an unconditional increment in the x coordinate:
( increment sprite/pos-x ) .sprite/pos-x LDZ2 INC2 .sprite/pos-x STZ2
we could check first if it has reached a specific amount, like the width of the screen, and not increment if that's the case:
.sprite/pos-x LDZ2 ( load x ) .Screen/width DEI2 #0008 SUB2 ( get screen width minus 8 ) EQU2 ( is x equal to screen width - 8 ? ) ,&continue JCN &increment ( increment sprite/pos-x ) .sprite/pos-x LDZ2 INC2 .sprite/pos-x STZ2 &continue
another possibility could be applying a modulo operation to our changed coordinates so that they always stay within the limits, returning to the left when crossing over the right, and viceversa.
a possible set of modulo macros could be:
%MOD { DUP2 DIV MUL SUB } ( a b -- a%b ) %MOD2 { OVR2 OVR2 DIV2 MUL2 SUB2 } ( a b -- a%b )
(there's a more optimized set but we'll discuss it later :)
we can apply those macros after incrementing or decrementing. for example:
( increment sprite/pos-x ) .sprite/pos-x LDZ2 INC2 .Screen/width DEI2 MOD2 ( apply modulo screen width ) .sprite/pos-x STZ2 ( store result )
another strategy for animation would consist in changing the sprite that is drawn at a specific position.
you could have a sequence of sprites/frames and animate them by running them in sequence!
for practical purposes i would recommend you to have a number of frames corresponding to a power of two, like 2, 4, 8, 16, 32, etc.
for example, the following is a sequence of eight 1bpp sprites corresponding to a diagonal line moving from the bottom right to the top left:
@animation &frame0 00 00 00 00 00 00 01 03 &frame1 00 00 00 00 01 03 06 0c &frame2 00 00 01 03 06 0c 18 30 &frame3 01 03 06 0c 18 30 60 c0 &frame4 03 06 0c 18 30 60 c0 80 &frame5 0c 18 30 60 c0 80 00 00 &frame6 30 60 c0 80 00 00 00 00 &frame7 c0 80 00 00 00 00 00 00
note that each frame consists in 8 bytes. that implies that there's an offset of 8 bytes between the addresses corresponding to each sublabel.
for example, the address of &frame1 would be 8 bytes more than the address of &frame0.
the frames you use could also be composed of 2bpp sprites. in that case, the offset between frames would be of 16 in decimal (10 in hexadecimal) bytes.
in order to have an animation consisting of those frames we need to change the address of Screen/addr at specific intervals so that it points to a different sprite each time.
how can we know the sprite address that we should be using at each frame?
one way of achieving it is having a "global variable" in the zero page that counts the frames of the program. also, we would need to have that count constrained in a range corresponding to the amount of frames in our animation.
we know already how to do the first part, and kind of know how to do the second!
in the zero page we declare the label for our framecount:
( zero page ) |0000 @framecount $1
and in the on-frame subroutine we increment it:
( increment framecount ) .framecount LDZ INC .framecount STZ
note that we are using a single byte to count, so it will go from 0 to 255 in a little bit more than 4 seconds, and then restart when overflowing its count.
for some applications it might be better to have a framecount in a short, that would count from 0 to 65535 and overflow in a little bit more than 18 minutes.
in order to have that framecount constrained to a range corresponding to our number of frames, we can use a modulo operation.
when we have a number of frames corresponding to a power of two, as recommended above, we can use an "AND mask" to perform this modulo operation faster than if we were using the MOD macros suggested previously.
for example, if we have 8 frames numbered from 0 to 7, we can notice that those numbers only require three bits to be represented.
to build our AND mask, we set as 1 those three bits, and 0 the others:
0000 0111: 07
this AND mask will "let pass" the three least significant bits of another byte, and turn off the others.
in uxntal this process would look as follows:
.framecount LDZ ( load framecount ) #07 AND ( apply AND mask, corresponding to modulo 8 )
the result of the operation will be a count going repeatedly from 0 to 7.
we could define this fast modulo operation as a macro to make the code more readable:
%8MOD { #07 AND } ( byte -- byte%8 )
if this wasn't too clear for you, i recommend you look back to uxn tutorial day 3, into the discussion of logic operations.
how can we use that count to select the sprite for the animation frame that we want to show?
we could use several conditional jumps, or we could use a more fun way that can be called pointer arithmetic :)
observe that the sublabel for the first frame (frame0) of our animation has the same address as the label for the whole animation. and, as we mentioned already, the next frame (frame1) starts 8 bytes afterwards.
the sublabel for each next frame is 8 bytes after the previous one.
or, another way to look at it:
generalizing, frameN is (N times 8) bytes after the animation label!
this means that if we get the absolute address for the animation label, and we add (N times 8) bytes to it, we'll get the absolute address for frameN :)
this amount of bytes that separates each sublabel is called an offset.
after applying the modulo 8 to our framecount, we can multiply it times 8 to get the offset with respect to the animation label:
.framecount LDZ ( load framecount ) 8MOD ( apply modulo 8 to obtain sequence between 0 and 7 ) #08 MUL ( multiply times 8 to get the offset )
note that so far we have been working with bytes, and everything has been fine.
however, absolute addresses are shorts!
this means we need to convert our offset to a short so that we can add it to the address of the animation data.
one way of doing it is with this macro that adds a 00 before the top element in the stack:
%TO-SHORT { #00 SWP } ( byte -- short )
our code would look as follows:
.framecount LDZ ( load framecount ) 8MOD ( apply modulo 8 to obtain sequence between 0 and 7 ) #08 MUL ( multiply times 8 to get the offset ) TO-SHORT ( convert to short )
another way, less clear but kind of fun (and slightly shorter in program memory), would consist in pushing the 00 before anything else happens:
#00 ( push high byte of the offset ) .framecount LDZ ( load framecount ) 8MOD ( apply modulo 8 to obtain sequence between 0 and 7 ) #08 MUL ( multiply times 8 to get the offset )
adding this offset to the address of our animation is comparatively simple:
.framecount LDZ ( load framecount ) 8MOD ( apply modulo 8 to obtain sequence between 0 and 7 ) #08 MUL ( multiply times 8 to get the offset ) TO-SHORT ( convert to short ) ;animation ( get animation address ) ADD2 ( add offset to address )
and then we could just send that to the Screen/addr port:
.Screen/addr DEO2 ( set computed address )
the program that does all this would look as follows.
note that it uses a sequence similar to the previous programs:
the clear sprite section is not actually needed in this case because of the colors that are used, but it would be when using colors with transparency in them :)
( hello-animation.tal ) ( devices ) |00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ] |20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1 ] ( macros/constants ) %HALF2 { #01 SFT2 } ( shift one bit to the right ) ( short -- short/2 ) %8MOD { #07 AND } ( byte -- byte%8 ) %TO-SHORT { #00 SWP } ( byte -- short ) %color-clear { #40 } ( clear 1bpp sprite from fg ) %color-2-3 { #4e } ( draw 1bpp sprite with color 2 and 3 ) ( zero page ) |0000 @framecount $1 ( init ) |0100 ( set system colors ) #2ce9 .System/r DEO2 #01c0 .System/g DEO2 #2ce5 .System/b DEO2 ( set screen/x and y to half of screen minus 4 ) .Screen/width DEI2 HALF2 #0004 SUB2 .Screen/x DEO2 .Screen/height DEI2 HALF2 #0004 SUB2 .Screen/y DEO2 ( set sprite address ) ;animation .Screen/addr DEO2 ( set screen vector ) ;on-frame .Screen/vector DEO2 BRK @on-frame ( -> ) ( 0: increment framecount ) .framecount LDZ INC .framecount STZ ( 1: clear sprite ) ( clear sprite from the fg ) color-clear .Screen/sprite DEO ( 2: update sprite address ) .framecount LDZ ( load framecount ) 8MOD ( apply modulo 8 to obtain sequence between 0 and 7 ) #08 MUL ( multiply times 8 to get the offset ) TO-SHORT ( convert to short ) ;animation ( get animation address ) ADD2 ( add offset to address ) .Screen/addr DEO2 ( set computed address ) ( draw sprite in fg with color 2 and 3 ) color-2-3 .Screen/sprite DEO BRK ( sprite data ) @animation &frame0 00 00 00 00 00 00 01 03 &frame1 00 00 00 00 01 03 06 0c &frame2 00 00 01 03 06 0c 18 30 &frame3 01 03 06 0c 18 30 60 c0 &frame4 03 06 0c 18 30 60 c0 80 &frame5 0c 18 30 60 c0 80 00 00 &frame6 30 60 c0 80 00 00 00 00 &frame7 c0 80 00 00 00 00 00 00
it wasn't as complicated, was it? :) this example includes many concepts that are worth studying, so i invite you to read it carefully!
for some fun possibilities, i invite you to draw the tile multiple times in different places and possibly with different flipping modes! that may generate more interesting animations!
or, even better, design and use your own sprites!
until now, everything we have been doing has happened at 60 frames per second, that may be too fast for some applications!
fortunately, we can use some simple arithmetic with our framecount in order to slow down its effects.
for example, if we want to update our frames at half that speed (30 frames per second), we can divide over two the value of framecount before applying the modulo.
as you may remember this division can be done with SFT in the case of powers of two, or with DIV for any other case.
%HALF { #01 SFT } ( byte -- byte/2 ) %QUARTER { #02 SFT } ( byte -- byte/4 ) %EIGHTH { #03 SFT } ( byte -- byte/8 )
we can use these macros to divide the frequency in our code:
( 2: update sprite address ) .framecount LDZ ( load framecount ) QUARTER ( divide over 4 the frequency ) 8MOD ( apply modulo 8 to obtain sequence between 0 and 7 ) #08 MUL ( multiply times 8 to get the offset ) TO-SHORT ( convert to short ) ;animation ( get animation address ) ADD2 ( add offset to address ) .Screen/addr DEO2 ( set computed address )
ah, way better!
note that if you want to divide the frequency to numbers that are not powers of 2, you might start to see some glitches approximately every 4 seconds: this is due to framecount overflowing and not giving a nice sequence of results for those divisors.
this might also happen if you have an animation consisting of a number of frames that is not a power of 2, and you use a normal MOD operation to calculate the offset to the corresponding frame.
the easiest workaround for these issues would be to use a short-sized framecount that would only cause those overflow glitches at approximately every 18 minutes.
you'd have to adapt the program to work with that size of framecount - nice exercise, i feel and think!
these are all the uxntal instructions that we discussed today!
the addresses for LDA and STA are always shorts, while the addresses for the other instructions are always one byte.
in short mode, these instructions load or store shorts from or into memory.
in uxn tutorial day 5 we introduce the varvara mouse device to explore more possible interactions, and we cover the remaining elements of uxntal and uxn: the return stack, the return mode and the keep mode.
we also discuss possible structures to create loops and more complex programs using these resources!
first, i invite you to take a break!
then, keep exploring, and share your findings!
if you enjoyed this tutorial and found it helpful, consider sharing it and giving it your support :)
uxn tutorial deprecated appendix a
text, images, and code are shared with the peer production license