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.