zanto wrote: ↑Tue Mar 09, 2021 8:19 pm
There was some discussion about the usage of stacks or not.
It's not about using stacks or not. It's about absuing the stack pointer register as an additional register that you can use like A, X and Y.
I.e. you can write stuff to the stack and pull stuff from it. What you shouldn't do is manipulating the stack register yourself.
You initialize the stack pointer register once at the beginning. But from then on, you should never write any values there anymore.
I.e. if register S has the number 25 in it, then you don't take the number 25, save it in a variable and then use S to fill with your own temporary counter variables, only to write the 25 back into S at the end.
You don't do this because the 25 is a number that tells the processor where the memory address for the next JSR gets written and where the memory address for the next RTS is located. If you use S for a temporary storage and then call another function, your program flow is dead. If you use S for a temporary storage and the NMI hits, your program flow is dead.
This is what you should never do: Using TXS at any location except for the start. Because that's just insane.
Of course you can still use the stack itself via PHA/PLA.
zanto wrote: ↑Tue Mar 09, 2021 8:19 pm
I could change it if there's a more practical reason to do so (like chaining branches consumes less memory and clocks, which it doesn't seem like they do).
It might be shorter, but sometimes you need to make a decision: Are you really in such a need to save two or three cycles in a branch instruction, so that you need to make your code dirty?
zanto wrote: ↑Tue Mar 09, 2021 8:19 pm
The entity/sprite management system. This will be a big issue in the games that I'd like to develop on the NES, so having a grasp on what's the best approach to this kind of thing would be super helpful!
This is in fact something that might be a bit more complicated depending on how you want to show your sprites.
I know you think of a simple loop where you simply blast your data into the hardware sprites, but this might not suffice.
Think of the situation when you want to flip your sprites. Now you don't only flip the hardware sprites themselves. You also have to draw your sprites from right to left.
Or think about this: You can store the palette values for four sprite tiles into one byte since each palette value only requires two bits. Hence, you need a mechanism to shift the palette values to then cut off the lowest bits and apply them to the sprite attributes of the current tile.
The sprite rendering function might become quite long since it might require a lot of logical calculations. But this is not an NES-specific topic. This is just programming in general: How to prepare your sprite data before drawing it into the hardware sprites.
zanto wrote: ↑Tue Mar 09, 2021 8:19 pm
Codes that go in the NMI routine or the "normal" code section. As I said before I transferred some code that was in the NMI routine to the normal code block. Should I always make absolutely sure that the NMI routine is 100% optimized or are there situations when it's okay to have normal code there?
Logic in the game loop, graphic updates in NMI. But preparation of graphic updates still in the game loop.
This is the go-to article for this kind of topic:
https://wiki.nesdev.com/w/index.php/The_frame_and_NMIs
Regarding the usage of variables:
For entities on screen, I have something like this:
Code: Select all
struct Characters
{
byte Type[MaxNumberOfCharacters];
byte X[MaxNumberOfCharacters];
byte Y[MaxNumberOfCharacters];
byte FacingDirection[MaxNumberOfCharacters];
byte Value1[MaxNumberOfCharacters];
byte Value2[MaxNumberOfCharacters];
byte Value3[MaxNumberOfCharacters];
byte Value...[MaxNumberOfCharacters];
} AllCharacters;
I have as many Value variables as the entity type that needs the most values.
And MaxNumberOfCharacters is of course the maximum number of entities that you can have on the screen at once.
If you load, for example, an opponent into slot number 3, you set the Type[3] variable to TypeOpponent, you set its X[3] and Y[3] position. And then you set everything that is opponent-specific to the Value variables.
I use macros to rename the generic variables accordingly:
Code: Select all
#define OppEnergy Value1
#define OppMovementPattern Value2
#define OppMovementPatternIndex Value3
// Weapons:
#define WpnHorizontalMovingDirection Value1
#define WpnVerticalMovingDirection Value2
#define WpnSpeed Value3
All of this can also be done in Assembly code.
Now you have the minimum necessary variables for entities on screen.
For local variables, i.e. variables that are only needed within a function or as function parameters, I did this:
I reserved several groups of variables:
Byte1A, Byte2A, Byte3A etc.
Byte1B, Byte2B, Byte3B etc.
etc.
If a function doesn't call another function, its name gets a postfix "_a". And its local variables are taken from the A group.
If a function calls another function that is named "_a", then the current function uses the B group for its local variables and gets the name "_b". (To make sure that the "_a" function doesn't overwrite the local variables of the "_b" function.)
And so on: Check which functions your current function calls. And put your new function into the next group.
Again: Variable renaming via aliases:
Code: Select all
void MyFunction_b(void)
{
#define i Byte1B
#define max Byte2B
...
i = 5;
OtherFunction_a();
...
max = 23;
...
#undef i
#undef max
}
If MyFunction_b ever calls another function that already has a "_b", you need to rename MyFunction_b to MyFunction_c and change the macro definitions to Byte1C. Then let the compiler find the errors (since every occurence of MyFunction_b is now incorrect) and potentially also change the postfixes and variable references of the functions that call MyFunction_b/MyFunction_c.
This requires a bit of manual caution:
If you call a function with a "_c", but your own function already has a "_c" and you forget to update your function name to "_d", you might be in trouble.
Likewise, if you have a "_c" function, but your variable names still point to Byte1B or Byte1D.
But with the postfixes in the names, you can pretty easily check each function individually whether it applies to the standard. There aren't any global side effects to keep in mind. Every manual inspection is confined within each separate function.
So, if you have validated a single function as being named correctly and as using the correct group of variables and you don't change that function anymore, then no change anywhere else in the code can invalidate this already validated function.