A newbie's code evaluation

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

turboxray
Posts: 348
Joined: Thu Oct 31, 2019 12:56 am

Re: A newbie's code evaluation

Post by turboxray »

DRW wrote: Wed Mar 10, 2021 2:09 am
This is what you should never do: Using TXS at any location except for the start. Because that's just insane.
If the behavior is deterministic and works, it's not insane. Just because you're afraid to do something like that, doesn't mean it's not a viable option in whatever case scenario is applicable in. Is it for beginners? Obviously not. Is it an advance technique for squeezing out that last cycle? Absolutely yes. I have done crazier stuff than that (setting the cpu in a blitz for all unrolled copy routine and using timed interrupt to reign the cpu back in - modifying the stack so the RTI breaks out of that copy routine).
There's a difference between smart programming and doing something just because the hardware allows it. If the usage of a certain feature in a function is dependent on whether the function is called at around scanline 100 or scanline 200, this is the opposite of smart. Smart is if you write your functions robust and free of side effects.
Who hurt you?? Haha. I guess the demoscene coders aren't smart. I guess coders that push to get the most out of a system aren't smart. I dunno. I can write advance code and still refactor just fine. I mean, that simply comes with experience.
Last edited by turboxray on Wed Mar 10, 2021 3:42 pm, edited 4 times in total.
User avatar
Quietust
Posts: 1920
Joined: Sun Sep 19, 2004 10:59 pm
Contact:

Re: A newbie's code evaluation

Post by Quietust »

turboxray wrote: Wed Mar 10, 2021 9:45 am
DRW wrote: Wed Mar 10, 2021 2:09 am
This is what you should never do: Using TXS at any location except for the start. Because that's just insane.
If the behavior is deterministic and works, it's not insane. Just because you're afraid to do something like that, doesn't mean it's not a viable option in whatever case scenario is applicable in. Is it for beginners? Obviously not. Is it an advance technique for squeezing out that last cycle? Absolutely yes. I have don't crazier stuff than that (setting the cpu in a blitz for all unrolled copy routine and using timed interrupt to reign the cpu back in - modifying the stack so the RTI breaks out of that copy routine).
To be clear, there are scenarios in which you would want to use TXS like that, but those scenarios are extremely specific and so rare as to be effectively non-applicable to NES development (edit: and are thus not really worth discussing here).
Last edited by Quietust on Wed Mar 10, 2021 10:10 am, edited 1 time in total.
Quietust, QMT Productions
P.S. If you don't get this note, let me know and I'll write you another.
turboxray
Posts: 348
Joined: Thu Oct 31, 2019 12:56 am

Re: A newbie's code evaluation

Post by turboxray »

Quietust wrote: Wed Mar 10, 2021 9:52 am
turboxray wrote: Wed Mar 10, 2021 9:45 am
DRW wrote: Wed Mar 10, 2021 2:09 am
This is what you should never do: Using TXS at any location except for the start. Because that's just insane.
If the behavior is deterministic and works, it's not insane. Just because you're afraid to do something like that, doesn't mean it's not a viable option in whatever case scenario is applicable in. Is it for beginners? Obviously not. Is it an advance technique for squeezing out that last cycle? Absolutely yes. I have don't crazier stuff than that (setting the cpu in a blitz for all unrolled copy routine and using timed interrupt to reign the cpu back in - modifying the stack so the RTI breaks out of that copy routine).
To be clear, there are scenarios in which you would want to use TXS like that, but those scenarios are extremely specific and so rare as to be effectively non-applicable to NES development.
I thought that was kinda evident though. But practically non-applicable and never applicable are not the same thing. It's also a good mental exercise to think of solutions outside the box, if you care about performance or pushing the system. But calling it insane is just ignorant.
User avatar
Quietust
Posts: 1920
Joined: Sun Sep 19, 2004 10:59 pm
Contact:

Re: A newbie's code evaluation

Post by Quietust »

turboxray wrote: Wed Mar 10, 2021 10:01 am
Quietust wrote: Wed Mar 10, 2021 9:52 am To be clear, there are scenarios in which you would want to use TXS like that, but those scenarios are extremely specific and so rare as to be effectively non-applicable to NES development.
I thought that was kinda evident though. But practically non-applicable and never applicable are not the same thing. It's also a good mental exercise to think of solutions outside the box, if you care about performance or pushing the system. But calling it insane is just ignorant.
Keep in mind that this is the "Newbie Help Center" forum, and any advanced coding techniques that involve the usage of the stack pointer as a general-purpose register are all but guaranteed to be wholly inappropriate for newbies, so there's little point in discussing them here, much less actively recommending their use.
Quietust, QMT Productions
P.S. If you don't get this note, let me know and I'll write you another.
User avatar
zanto
Posts: 57
Joined: Sun Mar 07, 2021 11:15 pm
Location: Rio de Janeiro, Brazil

Re: A newbie's code evaluation

Post by zanto »

Quietust wrote: Wed Mar 10, 2021 9:52 am
turboxray wrote: Wed Mar 10, 2021 9:45 am
DRW wrote: Wed Mar 10, 2021 2:09 am
This is what you should never do: Using TXS at any location except for the start. Because that's just insane.
If the behavior is deterministic and works, it's not insane. Just because you're afraid to do something like that, doesn't mean it's not a viable option in whatever case scenario is applicable in. Is it for beginners? Obviously not. Is it an advance technique for squeezing out that last cycle? Absolutely yes. I have don't crazier stuff than that (setting the cpu in a blitz for all unrolled copy routine and using timed interrupt to reign the cpu back in - modifying the stack so the RTI breaks out of that copy routine).
To be clear, there are scenarios in which you would want to use TXS like that, but those scenarios are extremely specific and so rare as to be effectively non-applicable to NES development (edit: and are thus not really worth discussing here).
DRW wrote: Wed Mar 10, 2021 2:09 am 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.
DRW wrote: Wed Mar 10, 2021 2:09 am 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.
I can see using the S registers being a way to tackle specific problems. However I don't think I'd want mess around with it at the moment, more because I don't trust myself to be vigilant enough to make sure it won't be invalid when I need it. :roll:



-----------------------------
DRW wrote: Wed Mar 10, 2021 2:09 am
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.
I thought about this and a lot of these issues you brought up could be handled by the entity's variables and if they are changed, they can update the sprite (e.g: the entity has a direction (left/right) variable and there's a set_direction function that updates an instance's direction and update's the instance's sprite data in memory if needed. However, this raises the concern of memory use to save all the entity data that I brought up in my original post.



-----------------------------
DRW wrote: Wed Mar 10, 2021 2:09 am
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
Thanks! I'll take a look at that article!



-----------------------------
DRW wrote: Wed Mar 10, 2021 2:09 am 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
I assume each type of entity (player, weapon, enemy etc) can have a different number of variables, right? This brings up an issue I was thinking about last night and still have no idea how to deal effectively. Suppose you have these entity structures: A has 4 bytes, B has 5 bytes and C has 2 bytes. And in the RAM (or ZP), you allocated memory for instances of each instances in the following order: (in this example, I'll make the bytes for each type of entity have the same letter as the entity for the sake of clarity (so AA is a byte allocated for an instance of entity A)

Code: Select all

AA AA AA AA CC CC BB BB BB BB BB CC CC CC CC AA AA AA AA
Meaning, in this memory area, you have one instance of A, followed by one instance of C, then one B, then two instances of C. Now, suppose you erase the first instance of C and you tried to delete it from memory. That area would end up something like:

Code: Select all

AA AA AA AA 00 00 BB BB BB BB BB CC CC CC CC AA AA AA AA
Now suppose you wanted to delete another instance of C, the first one in the memory again, it'd end up like this:

Code: Select all

AA AA AA AA 00 00 BB BB BB BB BB 00 00 CC CC AA AA AA AA
In this area there are 2 empty sub-areas, each with 2 bytes. So even though this area has 4 bytes of free memory, if I wanted to create another instance of A, which is 4 bytes long, I wouldn't be able to add it here, because the 4 bytes are split into 2 different areas. How do you deal with something like this? Do you shift all bytes left after cleaning up a memory space? It sounds like it'd be a bottle neck in a NES game...

Also, would you store all this data on ZP or the RAM? I feel like ZP would be too limited if the instances has a lot of variables and you share the ZP with other kinds of data that is not related to the instances.
Last edited by zanto on Wed Mar 10, 2021 1:07 pm, edited 2 times in total.
turboxray
Posts: 348
Joined: Thu Oct 31, 2019 12:56 am

Re: A newbie's code evaluation

Post by turboxray »

Quietust wrote: Wed Mar 10, 2021 10:14 am
Keep in mind that this is the "Newbie Help Center" forum, and any advanced coding techniques that involve the usage of the stack pointer as a general-purpose register are all but guaranteed to be wholly inappropriate for newbies, so there's little point in discussing them here, much less actively recommending their use.
Keep in mind, I'm completely aware of this and my response has nothing to do with that. I'm just just calling out comment/mentality. I thought that was apparent.
User avatar
DRW
Posts: 2225
Joined: Sat Sep 07, 2013 2:59 pm

Re: A newbie's code evaluation

Post by DRW »

zanto wrote: Wed Mar 10, 2021 10:58 am I thought about this and a lot of these issues you brought up could be handled by the entity's variables and if they are changed, they can update the sprite (e.g: the entity has a direction (left/right) variable and there's a set_direction function that updates an instance's direction and update's the instance's sprite data in memory if needed. However, this raises the concern of memory use to save all the entity data that I brought up in my original post.
You wouldn't update the sprite data along with the direction. Writing the sprites would be a final step after all the game logic is done.

So, I'd suggest this:

For an entity, you have the x and y position, the facing direction and an index to the current animation frame. You handle your game logic without caring for the actual sprites (except for setting that index to the current animation frame, for example during a walk cycle).

Then you have a general DrawSpritesForEntity function that you call at the end, once per entity.

This one takes the animation frame index to find out which meta sprite (i.e. which sprite data collection) you want to draw. I.e. from the index, you check whether you want to draw MarioStanding, MarioJumping, MarioWalking1, MarioWalking2, MarioWalking3, Goomba1, Goomba2 etc.
Those meta sprites are purely in ROM. You don't store them in any variables. You merely have an index or a pointer for your entity that says which meta sprite shall be drawn for it next.

The facing direction decides where your drawing routine puts your first hardware sprite (on the left or on the right, relative from your entity's center-x position) and whether you increment or decrement the local x position counter after each step, to draw the entity in its regular order or in its mirrored order.
Likewise, the facing direction decides whether you set the horizontal mirror bit for each hardware sprite.

zanto wrote: Wed Mar 10, 2021 10:58 am I assume each type of entity (player, weapon, enemy etc) can have a different number of variables, right? This brings up an issue I was thinking about last night and still have no idea how to deal effectively.
I understand your issue, but you have misunderstood me in one specific detail:

Those aren't data that you allocate and destroy as needed. Instead, the struct that I listed is one big chunk of global variables. They are always there.

On the NES, you are limited in your hardware sprites anyway, so there's really no need to create and delete entity variables like in a linked list.
You simply decide in advance: What is the maximum number of entities you will ever have on screen at once? This is your array length.

And then you have one array for each necessary property:
Type, X, Y and FacingDirection are probably needed for every entity type.
And then you have generic Value1, Value2, Value3 that you then use individually per entity type, i.e. for an opponent, the energy is Value1. A weapon doesn't have an energy, so when it's a weapon, Value1 is used for something else.
(That's why I said use aliases: OppEnergy = Value1, OppMovementPattern = Value2, WpnSpeed = Value1 etc.)

In Assembly it might look like this:

Code: Select all

EntityCount = 5
EntityType: .res EntityCount
EntityX: .res EntityCount
EntityY: .res EntityCount
EntityFacingDirection: .res EntityCount
EntityValue1: .res EntityCount
EntityValue2: .res EntityCount
EntityValue3: .res EntityCount
If EntityType[CurrentEntityIndex] equals TypeNone, this means this entity is disabled:

Code: Select all

LDX CurrentEntityIndex
LDA EntityType, X
CMP #TypeNone
BEQ @entityProcessingEnd
You can use this free slot to "create" a new entity. (Creation means you set the EntityType variable to TypeOpponent or TypeWeapon or whatever and then initialize the necessary variables.)
If you want to "delete" an entity, you simply set EntityType[CurrentEntityIndex] to TypeNone again. You don't even need to clean up the other variables. Leave them as they are. They will be overwritten once you initialize a new entity in this slot.

And then you do a loop for the game logic, something like:

Code: Select all

for (CurrentEntityIndex = 0; CurrentEntityIndex < EntityCount; ++CurrentEntityIndex)
{
    switch (EntityType[CurrentEntityIndex])
    {
    case TypeNone:
        // Do nothing.
        break;
        
    case TypeOpponent:
        ProcessCurrentOpponent();
        break;
        
    case TypeWeapon:
        ProcessCurrentWeapon();
        break;
        
    // ...
    }
}
And in the end, the same for drawing the sprites:

Code: Select all

for (CurrentEntityIndex = 0; CurrentEntityIndex < EntityCount; ++CurrentEntityIndex)
{
    if (EntityType[CurrentEntityIndex] != TypeNone)
        DrawSpritesForCurrentEntity();
}
If CurrentEntityIndex is also a global variable, you don't need to pass the index as a parameter to ProcessCurrentOpponent, DrawSpritesForCurrentEntity etc. because these functions can use the global variable directly.

zanto wrote: Wed Mar 10, 2021 10:58 am Also, would you store all this data on ZP or the RAM? I feel like ZP would be too limited if the instances has a lot of variables and you share the ZP with other kinds of data that is not related to the instances.
I'd store as much as possible in zeropage. If it doesn't fit, I'd put the lesser-needed variables into regular RAM.
The good thing here is: You can decide this for every property array individually.
My game "City Trouble":
Gameplay video: https://youtu.be/Eee0yurkIW4
Download (ROM, manual, artworks): http://www.denny-r-walter.de/city.html
User avatar
zanto
Posts: 57
Joined: Sun Mar 07, 2021 11:15 pm
Location: Rio de Janeiro, Brazil

Re: A newbie's code evaluation

Post by zanto »

DRW wrote: Wed Mar 10, 2021 2:03 pm For an entity, you have the x and y position, the facing direction and an index to the current animation frame. You handle your game logic without caring for the actual sprites (except for setting that index to the current animation frame, for example during a walk cycle).

Then you have a general DrawSpritesForEntity function that you call at the end, once per entity.
I see. My idea was to update only the necessary sprite bytes during game logic to avoid having to iterate through all the instance's sprite bytes and update them after all logic was done. That's why in my entity structure, there's one byte for the offset in the sprite memory ($02XX), so I know which byte in memory I'd have to update in case I wanted to draw an instance in a new y position or with a new metatile. So I'd sacrifice one byte for each instance trying to save processing time to draw entities. I'm not really sure if this is a good approach, though. It's still hard for me to know for sure whether a code is more or less efficient than another (having to check byte number and clock time for each instruction manually - specially since I don't have those memorized - is such a pain).


--------------
DRW wrote: Wed Mar 10, 2021 2:03 pm Those aren't data that you allocate and destroy as needed. Instead, the struct that I listed is one big chunk of global variables. They are always there.

On the NES, you are limited in your hardware sprites anyway, so there's really no need to create and delete entity variables like in a linked list.
You simply decide in advance: What is the maximum number of entities you will ever have on screen at once? This is your array length.
I see what you mean now. But if I understood your solution, it worries me that I may end up with many unused bytes. For example, a player entity would have many more bytes than a bullet entity, since it can have much more complex behavior.
So I wonder if I could create multiple types of entities and allocate different address ranges for them in ZP. Like I could create a BigEntity with 10 bytes, define I could have up to 10 of those on screen. So I know that they take 100 bytes in total and can be stored in address range $00-$63, for example. Then I create a SmallEntity with 3 bytes define there can be up to 20 of those, for a total of 60 bytes that can be stored in $64-$9F. Maybe that would mitigate the problem...
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: A newbie's code evaluation

Post by tokumaru »

zanto wrote: Wed Mar 10, 2021 3:32 pmMy idea was to update only the necessary sprite bytes during game logic to avoid having to iterate through all the instance's sprite bytes and update them after all logic was done. That's why in my entity structure, there's one byte for the offset in the sprite memory ($02XX), so I know which byte in memory I'd have to update in case I wanted to draw an instance in a new y position or with a new metatile. So I'd sacrifice one byte for each instance trying to save processing time to draw entities. I'm not really sure if this is a good approach, though.
Games usually build all sprites from scratch every frame for the purpose of cycling sprite priorities, trading sprite dropout for flickering when there are too many sprites. It's generally considered bad practice to hardcode sprites to specific OAM positions.
But if I understood your solution, it worries me that I may end up with many unused bytes. For example, a player entity would have many more bytes than a bullet entity, since it can have much more complex behavior.
If you have only one player, you can simply store any additional information it needs in common variables, separate from the arrays of properties that are common to all objects. If you have 2 or more simultaneous players, you can create additional smaller arrays for the properties that only players have, and make sure to always assign players to the lowest numbered slot, in a way that, say, objects in slots 0 and 1 have access to extra properties in small 2-element arrays that the remaining objects don't have.
So I wonder if I could create multiple types of entities and allocate different address ranges for them in ZP.
ZP is a terrible place to store arrays, because when you use indexing and/or indirection, ZP becomes as slow as the rest of RAM, so you're wasting precious ZP memory for nothing. Better save ZP for standalone variables you use all the time, and also for local/temporary variables.
Like I could create a BigEntity with 10 bytes, define I could have up to 10 of those on screen. So I know that they take 100 bytes in total and can be stored in address range $00-$63, for example. Then I create a SmallEntity with 3 bytes define there can be up to 20 of those, for a total of 60 bytes that can be stored in $64-$9F. Maybe that would mitigate the problem...
Sequential bytes are much slower to access unless you're using 256 bytes or less for the whole thing, because you need to use indirect indexed addressing to access the data. Another problem with this approach is fragmentation - as you activate and deactivate objects using different amounts of RAM, the monkey becomes fragmented, and you may have difficulties finding space for larger objects because you don't have enough contiguous memory, just a bunch of small holes.
User avatar
DRW
Posts: 2225
Joined: Sat Sep 07, 2013 2:59 pm

Re: A newbie's code evaluation

Post by DRW »

As tokumaru said, sprite updates should be done anew every time, so you can implement sprite flickering for more than eight sprites per scanline or differently-sized entities and stuff.

zanto wrote: Wed Mar 10, 2021 3:32 pm I see what you mean now. But if I understood your solution, it worries me that I may end up with many unused bytes.
You end up with the exact amount of bytes that you need in an extreme case: If your opponents have the most properties and you can have 10 opponents on screen, then you need all those variables, right? That they are unused 99% of the time, when only five opponents and some projectiles are on screen, doesn't matter: You cannot share these variables for anything else anyway since you never know when 10 opponents are actually on screen. Hence, you don't waste anything.

zanto wrote: Wed Mar 10, 2021 3:32 pm For example, a player entity would have many more bytes than a bullet entity, since it can have much more complex behavior.
Well, a player is something completely different anyway since it doesn't disappear and another entity gets loaded into its slot. So, whatever variables are specific to the player can go into their own dedicated variables and can be outside those arrays.

zanto wrote: Wed Mar 10, 2021 3:32 pm So I wonder if I could create multiple types of entities and allocate different address ranges for them in ZP.
Different groups are fine, sure. If you declare that your 10 entities cannot be 10 opponents or 10 projectiles or any other combination of both, but that you can only have up to five opponents plus up to five projectiles (i.e. still 10 entities, but not in any combination, but five per type at the most), then yes: You can declare separate arrays.

However, as tokumaru said: Don't use sequential memory. Because if you need the fifth value of the nth opponent (and if 10 opponents are possible), you need to calculate CurrentIndex * 10 + 5 to get to the memory location. And multiplication is expensive.
If you have five distinct, independent arrays of 10 bytes each, you simply need to do

Code: Select all

LDX CurrentIndex
LDA Value5, X
So, let's say you have two players, five opponents and four other things. The other things can be either projectiles or collectable items.
Then your code might look like this:

Code: Select all

PlayerX: .res 2
PlayerY: .res 2
PlayerFacingDirection: .res 2
PlayerWhatever: .res 2

OpponentIsActive: .res 5
OpponentX: .res 5
OpponentY: .res 5
OpponentFacingDirection: .res 5
OpponentMovementPattern: .res 5
OpponentMovementPatternIndex: .res 5
OpponentWhatever: .res 5

ProjectileOrItemType: .res 4
ProjectileOrItemX: .res 4
ProjectileOrItemY: .res 4
ProjectileOrItemFacingDirection: .res 4
ProjectileOrItemGenericValue1: .res 4
ProjectileOrItemGenericValue2: .res 4

ProjectileSpeed = ProjectileOrItemGenericValue1
ProjectileMovingDirection = ProjectileOrItemGenericValue2

ItemQuantity = ProjectileOrItemGenericValue1
ItemDurationUntilDisappearance = ProjectileOrItemGenericValue2
(Of course, you would define the numbers as constants.)

Then you need several loops:

Code: Select all

for (CurrentIndex = 0; CurrentIndex < 2; ++CurrentIndex)
    ProcessCurrentPlayer();

for (CurrentIndex = 0; CurrentIndex < 5; ++CurrentIndex)
{
    if (OpponentIsActive[CurrentIndex])
        ProcessCurrentOpponent();
}

for (CurrentIndex = 0; CurrentIndex < 4; ++CurrentIndex)
{
    switch (ProjectileOrItemType[CurrentIndex])
    {
    case ProjectileOrItemTypeInactive:
        // Do nothing.
        break;
        
    case ProjectileOrItemTypeProjectile:
        ProcessCurrentProjectile();
        break;
        
    case ProjectileOrItemTypeItem:
        ProcessCurrentItem();
        break;
    }
}

tokumaru wrote: Wed Mar 10, 2021 3:57 pm ZP is a terrible place to store arrays, because when you use indexing and/or indirection, ZP becomes as slow as the rest of RAM, so you're wasting precious ZP memory for nothing.
Really? That was new to me.
Last edited by DRW on Wed Mar 10, 2021 5:51 pm, edited 1 time in total.
My game "City Trouble":
Gameplay video: https://youtu.be/Eee0yurkIW4
Download (ROM, manual, artworks): http://www.denny-r-walter.de/city.html
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: A newbie's code evaluation

Post by tokumaru »

DRW wrote: Wed Mar 10, 2021 5:37 pm
tokumaru wrote: Wed Mar 10, 2021 3:57 pm ZP is a terrible place to store arrays, because when you use indexing and/or indirection, ZP becomes as slow as the rest of RAM, so you're wasting precious ZP memory for nothing.
Really? That was new to me.
Just look at the instruction times: http://www.obelisk.me.uk/6502/reference.html

ZP addressing is 1 cycle faster than Absolute addressing (3 cycles vs. 4), but ZP Indexed is just as fast/slow as Absolute Indexed (both are 4 cycles). Putting arrays that you'll be accessing with index registers in ZP is a waste of ZP. It still saves a byte though, if that's important to you, but generally I consider ZP a more precious resource than PRG-ROM.
User avatar
zanto
Posts: 57
Joined: Sun Mar 07, 2021 11:15 pm
Location: Rio de Janeiro, Brazil

Re: A newbie's code evaluation

Post by zanto »

tokumaru wrote: Wed Mar 10, 2021 3:57 pmGames usually build all sprites from scratch every frame for the purpose of cycling sprite priorities, trading sprite dropout for flickering when there are too many sprites. It's generally considered bad practice to hardcode sprites to specific OAM positions.
I understood the issue with sprite priorities (which is not a real concern in this game I'm working on), but not the flickering thing. Could you elaborate? (I know what flickering is, I just didn't understand how one thing affects the other)


tokumaru wrote: Wed Mar 10, 2021 3:57 pmIf you have only one player, you can simply store any additional information it needs in common variables, separate from the arrays of properties that are common to all objects. If you have 2 or more simultaneous players, you can create additional smaller arrays for the properties that only players have, and make sure to always assign players to the lowest numbered slot, in a way that, say, objects in slots 0 and 1 have access to extra properties in small 2-element arrays that the remaining objects don't have.
Maybe using player as an example wasn't a good idea, since games usually have a pre-determined amount of players at once (1-2). I was thinking more about other bigger entities which are harder to control (e.g. the amount of enemies on screen).


tokumaru wrote: Wed Mar 10, 2021 3:57 pmZP is a terrible place to store arrays, because when you use indexing and/or indirection, ZP becomes as slow as the rest of RAM, so you're wasting precious ZP memory for nothing. Better save ZP for standalone variables you use all the time, and also for local/temporary variables.
Since most of the data I'm storing in these arrays aren't address bytes, just pure values, I haven't been using indirect addressing, just zero page and indexed zero page addressing. So that's not so bad, right?


tokumaru wrote: Wed Mar 10, 2021 3:57 pmSequential bytes are much slower to access unless you're using 256 bytes or less for the whole thing, because you need to use indirect indexed addressing to access the data.
But with my idea, I think I'll use less than 256 bytes because of the limitations of how many entities I can have on screen at once... unless the entities end up having too many bytes each, which I imagine they won't.


tokumaru wrote: Wed Mar 10, 2021 3:57 pmAnother problem with this approach is fragmentation - as you activate and deactivate objects using different amounts of RAM, the monkey becomes fragmented, and you may have difficulties finding space for larger objects because you don't have enough contiguous memory, just a bunch of small holes.
This is the issue I brought up in my previous post, which is one of the reasons I imagine DRW mentioned that you can just make every entity have the same number of bytes, so if you don't run into the problem of a bigger entity "not fitting in a small hole". But being afraid of all the unused bytes all the entities may end up causing, I thought about dividing my memory into 2 segments. One that only stores entities with lots of bytes and the other that stores entities with less bytes. You wouldn't be able to store a big entity in a small entity segment and vice-versa. Also all big entities have the same number of bytes. The same goes for the small entities. That would mitigate the segmentation problem, right?
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: A newbie's code evaluation

Post by tokumaru »

zanto wrote: Wed Mar 10, 2021 5:48 pmI understood the issue with sprite priorities (which is not a real concern in this game I'm working on), but not the flickering thing. Could you elaborate? (I know what flickering is, I just didn't understand how one thing affects the other)
The NES doesn't flicker sprites by itself, it simply drops the ones with lowest priorities if there are more than 8 aligned horizontally. Most games don't want their sprites disappearing, as that could make the game unplayable (e.g. invisible enemies and projectiles), so they shuffle the OAM positions every frame, so that no particular sprite stays locked with a specific priority. This causes different sprites to be dropped each frame, creating the flickering effect.
Maybe using player as an example wasn't a good idea, since games usually have a pre-determined amount of players at once (1-2). I was thinking more about other bigger entities which are harder to control (e.g. the amount of enemies on screen).
If you're gonna let each object use as much RAM as it wants, you'll end up with the fragmentation problem I mentioned before. You may think that you're saving resources by allocating RAM like that, but dynamic memory management is a pretty complicated thing to do right. Better prepare for the worst case and go with that. ONe way to not be so wasteful with the RAM is to have complex objects allocate more than 1 object slot, and store a references to the additional slots in the primary slot. This is a way to get more memory but still obtain and release blocks of a constant size.
Since most of the data I'm storing in these arrays aren't address bytes, just pure values, I haven't been using indirect addressing, just zero page and indexed zero page addressing. So that's not so bad, right?
It's not bad, but zero page indexed addressing offers no speed advantage over absolute indexed addressing. The instructions themselves are 1 byte shorter, but the execution time is the same. If you really want to save those ROM bytes, that's cool, but IMO there are better uses for ZP, if you ever have to choose.
But with my idea, I think I'll use less than 256 bytes because of the limitations of how many entities I can have on screen at once... unless the entities end up having too many bytes each, which I imagine they won't.
since you're using less than 256 bytes total this can be as fast as the more common solution of using parallel arrays (almost all games do that), but you'll still run into the fragmentation issue. If you allocate/deallocate memory for several simple entities that need only 16 bytes of RAM throughout the level, but when you reach a complex enemy and need 64 bytes for it, you may only have a bunch of 16-byte holes and not 64 consecutive bytes. That's a big problem.
But being afraid of all the unused bytes all the entities may end up causing, I thought about dividing my memory into 2 segments. One that only stores entities with lots of bytes and the other that stores entities with less bytes. You wouldn't be able to store a big entity in a small entity segment and vice-versa. Also all big entities have the same number of bytes. The same goes for the small entities. That would mitigate the segmentation problem, right?
Yeah, you could do that, but the program will be slightly more complex because of that. Is your game so demanding of memory that you really have to resort to special cases like this, instead of just doing what 95% of NES games have done for decades and is proven to work well? It's OK to break the patterns if you're going for something new and experimental, but why reinvent the wheel if you're just doing the good old bread and butter?
User avatar
zanto
Posts: 57
Joined: Sun Mar 07, 2021 11:15 pm
Location: Rio de Janeiro, Brazil

Re: A newbie's code evaluation

Post by zanto »

DRW wrote: Wed Mar 10, 2021 5:37 pm However, as tokumaru said: Don't use sequential memory. Because if you need the fifth value of the nth opponent (and if 10 opponents are possible), you need to calculate CurrentIndex * 10 + 5 to get to the memory location. And multiplication is expensive.
This is a very good point! I forgot that multiplication should be avoided in 6502 (unless it's by a power of 2).

tokumaru wrote: Wed Mar 10, 2021 6:12 pmThe NES doesn't flicker sprites by itself, it simply drops the ones with lowest priorities if there are more than 8 aligned horizontally. Most games don't want their sprites disappearing, as that could make the game unplayable (e.g. invisible enemies and projectiles), so they shuffle the OAM positions every frame, so that no particular sprite stays locked with a specific priority. This causes different sprites to be dropped each frame, creating the flickering effect.
I understand now, thank you for the clarification!
tokumaru wrote: Wed Mar 10, 2021 6:12 pmONe way to not be so wasteful with the RAM is to have complex objects allocate more than 1 object slot, and store a references to the additional slots in the primary slot. This is a way to get more memory but still obtain and release blocks of a constant size.
But if I allow entities to have more than one block, wouldn't that also create the fragmentation issue you mentioned before? Only instead of being in terms of bytes, it's blocks?

tokumaru wrote: Wed Mar 10, 2021 6:12 pmIt's not bad, but zero page indexed addressing offers no speed advantage over absolute indexed addressing. The instructions themselves are 1 byte shorter, but the execution time is the same. If you really want to save those ROM bytes, that's cool, but IMO there are better uses for ZP, if you ever have to choose.
Oh, I see what you mean now! That's why you said you don't gain that much using arrays in ZP. It makes sense now!

tokumaru wrote: Wed Mar 10, 2021 6:12 pmYeah, you could do that, but the program will be slightly more complex because of that. Is your game so demanding of memory that you really have to resort to special cases like this, instead of just doing what 95% of NES games have done for decades and is proven to work well? It's OK to break the patterns if you're going for something new and experimental, but why reinvent the wheel if you're just doing the good old bread and butter?
I apologize if I sounded pretentious discussing my idea. I know that the usual solutions for similar problems may not work on the NES because of its restrictions. But rather than just following some sort of rule, I like to know why it works better, so I can it more effectively or even adapt it if the need arises. Thanks to all the feedback and discussion in this thread, I'm learning a lot and I appreciate all the help!
tepples
Posts: 22708
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: A newbie's code evaluation

Post by tepples »

I worked on a game that had a simple actor pool/complicated actor pool split. The simple actors were called "bullets", used as either player projectiles, enemy projectiles, or environmental projectiles (rain, blowing leaves).
Post Reply