Here is a sequenced version, which helps showing the differences, by showin one effect at a time.
I think I will try one more version, an hybrid of version B and E. - B shows vertical movement (see previous tests), which I think is important,but it's too fast ( I was saving on frames for obvious reasons ). - E shows horizontal movement, which is important to give a sense of signal amplitude variation.
There is a Spectrum game, Airwolf, which also had a zapping like effect also, but it was on/off, like: turn on, a little after it kind of disintegrates.
I need a zapping effect that can be continuous which eventually can be enabled or disabled by game logic or user actions, hence it must look nice while animation is looping, but still look good, when disabling it.
Did a bunch of optimizations, and code reviews, on existing blit routines, and managed to squeeze out a few more T-States along the way.
After all these reviews, I still have to finish clipping Clipping was "forcing" me to start to procrastinate
Since free time is not very abundant, it must be used well, so I opted to start implementing something more visually appealing and motivating, like animations.
Two distinct animations can be seen, "ray" and "beam". Each animation has distinct speeds and number of frames:
"ray" (left) runs slower (for debugging) and uses 8 frames, each with pixel and color changes. "beam" (right) runs faster and uses 4 frames, each with changes only in color.
Still have to implement vertical tiling of sprites to implement complete rays and beams. Previous blit routines reviews were done with this objective in mind, so hopefully it will simplify the tiling task.
Again, thanks for the development log, and for showing the tricks and techniques you are using The game looks like its going to be one of those super frustrating and addictive as hell, am I right?
It's a good exercise to document the process and it might be useful to someone else reading it later on.
I'm certainly working to make it addictive, but also hopping it will not to be frustrating.
My major concern, right now, is performance. I'm almost certain that I won't be able to sustain 50 frames, and will eventually need to drop to 25 (which is still good), but will probably force me to use a back buffer, to keep graphic buffer changes from messing every odd visible frame.
Another concern, is that it seems I'm far behind, to be able to finish it by March. I might have to tone it down a bit (less features), to be able to finish on time.
I'm also hitting the dreaded "Out Of Symbol Space" again, due to new animation frames (DEFB/W in source), and limited available memory after loading the assembler and source code. I will probably have to switch to PC development and do cross-compiling, in the near future.
Current SOURCE code is near the 20.000 bytes mark.
Last night, I had to make a small refactor to the Sprite format, to make it easier and little faster to blit. However this implied to review all sprite handling routines: PixelBlit, ColorBlit, ReversePixelBlit, ReverseColorBlit, PixelBlitClipped, ABall, XBall and 4 scrollers functions. But it's already done, and working correctly. Not too much trouble, but I had to iron out a few bugs after the changes.
NOTE: Function names are expanded to be less cryptic.
New Animation Just after this, I added the Zap animation sprites I have shown last. Still need to add the electrode sprite. I made special arrangements so that two animations could be perfectly in synch, so that I can split an animation like the zapping animation in two parts, since electrode is a regular sprite, but the zapping ray will be a vertically tiled sprite.
Scrolling screen Currently I split the screen scrollable area in three parts: phase1, phase2, phase3.
Phase1 does level draw loop with Left Clipping Phase2 does level draw loop without any clipping Phase3 does level draw loop with Right Clipping
This is done, to optimize rendering and prevent variables to overflow, since 256 pixels in horizontal resolution is exactly what 8 bits (1 Byte) can code, so there is no room for negative values, or overshooting values required to determine clipping.
Phase1 and 2 are working. Phase3, still requires me to implement the pre-calc and draw loop.
Now I have to convince myself to finish Phase3, since clipping function already supports left and right clipping.
Apparently CPUs seem to know if a number is negative or positive, since they usually have a Sign flag (S flag in Z80 parlance).
However, you might be surprised to know that CPUs don't have a clue about what a negative number is. Besides the conventioned Sign and Overflow flag, the CPU is clueless for what is a negative/positive number.
By convention, and due to some peculiarities how binary logic was setup to work with numbers, it's most useful to define the most significant bit as the Sign bit. So when it is set (1) it's because it's a negative number. when it is reset (0) it's a positive number. In fact, the S flag is an exact copy of bit 7, it will become obvious why, further ahead.
There is a very peculiar mechanism, called "Two's Complement", that does all the magic for signed integer numbers.
By convention/definition the "Two's Complement" of a number is used to inverse the Sign of a number (Z80 Assembly "NEG" instruction).
For a number N it basically consists on these two steps: - negate or flip every bit in the number - add one (1) to the previous result.
The only exception is zero, which has no negative counter part. But if you apply the above rules, to zero, it still provides the correct result (zero).
Minus zero, is nonsense for computers and also in general, but don't say that to a mathematician, or you will get a deep lesson on limits or similar stuff
Quick example: So if we have an 8 bit number with decimal value 1, we get (00000001) binary. If we apply the above rules, we should get the representation of -1, which is:
1 - Inverse or negate number !(00000001) = (11111110) 2 - Add one (11111110)+(00000001) = (11111111)
(11111111) is a nice round number, it could be -1 or 255. Actually it can be both, it depends on how you look at it, with what conventions in mind.
Two's complement (that extra add), is the reason why negative numbers, always seem to have one more combination than positives (127 versus -128), but in fact the number of positives and negatives is the same, since ZERO is by convention positive.
So in practice the trick consists in setting things up so that when bit data overflow or underflow it wraps to the correct side, in the established convention.
Think of it like this: if you had a ruler that goes from 0 to 255, you could erase those numbers and rewrite them so that it starts at -128 and ends at +127 This way, you are sure to have a sequence of numbers that is incrementally sound, which allows us to transition from negative to positive values passing throw zero, which is what any civilized human would expect.
And why did the first computer techs decided to implement negative numbers like this ? Because it makes supporting negative numbers as simple as doing "almost" nothing.
It allows for a lot of math rules and conventions to keep working, with the already existing computer logic for increment, decrement, add, subtract, etc... Why, because in fact, you are doing the operation conceptually with signed integer numbers, but the computer is just working with unsigned integers, exactly as before.
It basically consists in offsetting the values, using conventions only, hence interpreting the allowed range (00000000 to 11111111) in a different way.
In practice, it was a very smart way to interpret the bits of a number, so that they wrap in the right place, allowing all existing operations to be used without change.
This was an exceptional idea to minimize complexity in the first days of computing, that prevailed to this day.
Take note of these:
- When you increment a negative number, it should get closer to zero Dec (-2) = Bin(11111110) After a regular bit increment it gets to Bin(11111111) which by convention is DEC -1, which is closer to zero.
- When we decrement a negative number, it should get further away from zero. which also checks up, when compared with the previous numbers (-1 decremented gives -2)
- Negating a number twice, should give the same number, so (-(-(N)) = N Which works as expected:
(continuing the 1 to -1 example above, and now performing -1 to 1 )
1 - Inverse or negate number !(11111111) = (00000000)
2 - Add one (00000000)+(00000001) = (00000001)
The only critical spot, is when we cross over from conventional max positive 127 = (01111111) to max negative -128 = (10000000), but this already happens with conventional unsigned numbers, but only in the transition from (11111111) to (00000000) and vice-versa.
After this, it should be easy to understand that when CPU is adding 2 + (-4) = -2, it's just a simple bit add (00000010 + 11111100 = 11111110). How the result is interpreted is what changes the meaning of the result.
Why is important to understand this ?
Because knowing it, allows us to take advantage of it, by devising interesting hacks or tricks.
A very simple example, is how to quickly extend an 8 bit signed number to a 16 bit signed number. I'll leave it as a simple exercise.
Another example, is to determine if an unsigned 8 bit number is >= 128 ? Just use the sign bit.
Same trick can be done for >= 32768 for a 16 bit unsigned number.
If you need to make something like y = -x -1 You might think you need two operations, swap signal on X, and ADD -1 or subtract 1.
But in fact, you can use just a single Z80 instruction, which is CPL (1's Complement) Why ?, Because NEG instruction does bit flip (same as CPL) but then adds 1. but then we need to decrement 1 (which cancels the add 1 from NEG), hence we can just use CPL.
If you have a positive number, that determines an horizontal padding distance (let's call it padLen), to the left and right of your 256 pixels on a screen line, how do you calc these positions ?
Left pixel position is the (padLen) value
Right pixel position is (256 - padLen) expression value
But since 256 is the overflow value of an unsigned 8 bit value, it can be simplified to (-padLen), which seems awkward, but works, since it only depends on how you look at the bits, as a negative number or has a positive number.
lets suppose that padLen = Decimal 5 = Binary (00000101)
If you compare the BLUE value with the RED value, you see that they are identical.
So Right Pixel position, can be calculated by just negating the 8 bit value, which in theory produces a negative number, which can be interpreted as a positive value.
LeftPixelPos = padLen RightPixelPos = -padLen
So no ADD or SUBtract is needed.
Resuming, in assembly, instead of:
LD HL,256 ; 256 doesn't fit in 8 bits, hence must use 16 bit arithmetic LD B,0 ; Assuming PadLen is Positive LD A,(PadLen) ; PadLen is an 8 bit value LD C,A AND A ; Reset Carry bit SBC HL,BC
LeftPixelPos is in A register Right PixelPos is in L register
it becomes as simple as:
LD A,(PadLen) LD B, A NEG
LeftPixelPos is in B register Right PixelPos is in A register
NOTE: if you are used to working with wrapping/overflow calcs, you could also assume that 256 = 0, hence 256 - PadLen <=> 0 - PadLen, which would also give you an hint that a simple NEG instruction would do the trick.
I had a suspicion that I had a bug in one of the color blit routines. So I created an interactive paint screen, with ink in Magenta, so that I could interactively test between frames, if something was wrong.
The screen is filled with ink in Magenta, just before painting the new frame.
Watching screenshot above, it is clear that reverse color blit routines still have a bug, in this case a miss alignment of 1 byte to the left, which I have to track and fix.
NOTE: Normal color blit is correct, since it never puts color where it shouldn't.
After some tests, the color position/address, was sometimes correctly aligned (2/8), but sometimes it wasn't.
The problem was due to a simple mistake. I was adding bytes to coords (y,x in DE), assuming it was already a screen address. I just had to reverse the operation, i.e. calc screen address first (based on y,x), and then add the amount of bytes required.
NOTE: in reverse color blitting, I'am processing data in reverse, i.e. from last to first, hence I need to determine the last byte position, so that I can loop back to beginning.
Here is a screenshot, showing that it's working, after this fix.
By coloring a blue background, it also allows to see exactly where I'm writing, including the extra space (the black area on the right of each sprite), that is used for sprite scrolling.
It also shows some debug info on bottom (hex data).
When using negative numbers, there is a caveat that we must be aware, because it pops a few bugs here and there, if we are not careful.
Z80, as well as other CPUs, has special arithmetic shift instructions, namely: SLA (Shift Left Arithmetic) and SRA (Shift Right Arithmetic).
And what is special about them ? Well, they know the Sign Flag, and that it maps to register bit 7 (an 8 bit register has, left to right, bit 7 to 0).
Hence when these instructions shift a register content, they make sure that the sign bit (7) is left untouched, since a single bit shift is the equivalent of multiply by 2 (shift left) or divide by 2 (shift right), so it should never change the sign of the value.
Ok, so when we need to divide the contents of a register by a power of 2 (2,4,8,16,32,64), we just do as many SRAs as we need. And it works as supposed.
Decimal (+13) = Binary(00001101)
So, SRA (00001101) = (00000110) = Dec (6) SRA (00000110) = (00000011) = Dec (3) SRA (00000011) = (00000001) = Dec (1) SRA (00000001) = (00000000) = Dec (0) SRA (00000000) = (00000000) = Dec (0)
NOTE: remember that this is integer division, dividing 13 or 12 by 2, will give 6 in both cases. The same goes for diving 3 or 2 by 2, will return 1 for both.
As can be seen, SRA keeps the sign bit at zero, which means it "injects" zeros into the number from the left, when shifting right.
The interesting fact, is that after enough SRAs, the number should reach ZERO. Once at ZERO, any further division has no further effect.
Divide a Negative Number
Now what happens, when we do the same with a negative number ?
NOTE: remember 2's complement rule (flip all bits, and add 1)
Decimal (-13) = Binary(11110011)
So, SRA (11110011) = (11111001) = Dec (-7) SRA (11111001) = (11111100) = Dec (-4) SRA (11111100) = (11111110) = Dec (-2) SRA (11111110) = (11111111) = Dec (-1) SRA (11111111) = (11111111) = Dec (-1) ...
Kind of weird isn't it ?
It isn't what we would expect!
It looks like the result is the same absolute value as positive operation, but added of 1. Ex: Abs( -7 ) = 7 <=> Abs( 6 ) +1
Here is a small reference table for the first negative numbers, to help confirm the values. -1 = 1111 1111 -2 = 1111 1110 -3 = 1111 1101 -4 = 1111 1100 -5 = 1111 1011 -6 = 1111 1010 -7 = 1111 1001 -8 = 1111 1000
So what can we do to divide a negative number by a power of 2, correctly ?
A simple and logic trick, would be to avoid the problem, by: - reverse the sign (hence make it positive) - do the required divides (shifts) - reverse the sign back (making it negative again, or zero).
NEG SRA SRA SRA NEG
This works, because NEG of ZERO is still ZERO
But from our previous observation, there is a simpler approach. We can correct division of negative numbers by adding 1 to the result!
SRA SRA SRA INC A
TIP: A single INC is faster than 2 NEGs
NOTE: both solutions require that we check the number is negative, before applying it. Applying any of these solutions to a positive number will return incorrect results.
And what about multiplying a negative number by a power of 2 using SLA ?
I leave that as an exercise, since this post is already too long.
After several iterations, I present the current Steel Ball Menu Design (Version 11) I'm not sure if it will be the final version, but it will certainly be something along these lines.
The main idea behind this design, was to escape from typical menus with just a list of text menu entries, plus some fancy title text.
What I imagined was a ZOOM IN version of what game play screen looks like.
Think of it like you are in the pits/garage of a sports race. You have the cars and systems up close, but when we go on into the race, we see them from further away. I believe this makes the Menu more part of the game, more in sync with the rest of the game, instead of something that you just have to put in there.
I had to minimize memory usage, hence I opted to provide some graphics that I could tile or repeat, allowing me to reuse the graphics. This allows to provide a nice screen with lots of graphics, without a big memory budget.
At first it might seem odd, because a static picture can't show movement, and hence it seems there is no way to select menu options. However, each beam emitter on the right side of the screen, will scroll between what is listed or selected and how to select it.
So: you see "1", but then you see "keyboard", and it loops. Several options can be scrolled/looped in the same beam, if needed.