Blades of the Lotus - Platformer

Moderator: Moderators

User avatar
Posts: 12209
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Blades of the Lotus - Platformer

Post by tokumaru »

You can have the meta-sprite origin be anywhere you want, you just need to adjust the origin's coordinates a bit to compensate for the fact that when sprites are flipped, their coordinates will not reference their top left corners, it can be the bottom/right, so you need to move the origin left/up by the width/height of a sprite.

I like the idea of having 4 loops that will add or subtract each coordinate as needed. It will occupy significantly more space, but you'll save a lot of space from not having to duplicate or quadruplicate your sprite definitions.

In my case I still prefer to include the pre-calculated versions of flipped X and Y values, because those can have the compensation for referencing the bottom/right corners baked in, so I don't need to adjust the origin at all.
Posts: 273
Joined: Wed May 13, 2020 8:31 am

Re: Blades of the Lotus - Platformer

Post by Goose2k »

If the tiles are centered around the origin, I think the calculation is simply:

Code: Select all

new_x_offset = (x_offset + 8) * -1;
render_x_pos = meta_x + new_x_offset;
This accounts for the fact that you want to go from positioning relative to the left edge of the sprite, to the right edge (" + 8 "), and then flips to the other side of the original ( "* -1" ).

Maybe that's just another way of saying what Doug suggested :P , but for some reason this makes more sense in my head. :wink:

Flipping around Y would be the same.

Here's some examples. Positions are in the window on the left:
This same formula does also work for non-centered meta-sprites, but will flip around the origin, which is likely not super useful. In an ideal world I think the center-point could be offset for the flip calculation, divorced from the actual render position of the meta sprite.
User avatar
Posts: 12209
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Blades of the Lotus - Platformer

Post by tokumaru »

Goose2k wrote: Sat May 07, 2022 10:12 pm

Code: Select all

new_x_offset = (x_offset + 8) * -1;
render_x_pos = meta_x + new_x_offset;
This might be expensive to do to EVERY offset though, so the first obvious optimization is to do the "+ 8" only once, to meta_x, before entering the sprite loop. The "* -1" part would still have to be done per-sprite, but you can either approximate it with "EOR #$ff" (which results in 1's complement values rather than 2's complement, but you can compensate for the off-by-one error in the previous step, while fixing meta_x), or you can have separate loops for the 4 flipping cases, where you subtract the offsets of flipped axes instead of adding them.
User avatar
Posts: 2112
Joined: Sat Sep 07, 2013 2:59 pm

Re: Blades of the Lotus - Platformer

Post by DRW »

In case you're interested, this is how I rendered sprites in "City Trouble", which is also a game where characters can be horizontally mirrored.

In my case, all characters on the screen are hardcoded with a width of two tiles.
The few times where a character is wider, I simply rendered two characters. Those were just the boss battles, so I could afford the wasteful CPU time.

Declaration in C:

Code: Select all

extern byte UpdateMetaSpritePalette_;
#pragma zpsym("UpdateMetaSpritePalette_")
extern int UpdateMetaSpriteX_;
#pragma zpsym("UpdateMetaSpriteX_")
extern byte UpdateMetaSpriteY_;
#pragma zpsym("UpdateMetaSpriteY_")

void __fastcall__ UpdateMetaSprite_(byte attributes);

/* The parametrized macro for UpdateMetaSprite_. */
#define UpdateMetaSprite(metaSprite, palette, x, y, attributes) \
{ \
    ConstPointer = metaSprite; \
    UpdateMetaSpritePalette_ = palette; \
    UpdateMetaSpriteX_ = x; \
    UpdateMetaSpriteY_ = y; \
    UpdateMetaSprite_(attributes); \
Assembly code:

Code: Select all

    ; void __fastcall__ UpdateMetaSprite_(byte attributes)
    ; Draws all sprites of one meta sprite
    ; into the variables in the sprites segment.

.segment "ZEROPAGE"

    ; The index in the sprites segment
    ; where drawing is done.
    UpdateSpritesPpuSpriteIndex: .res 1
    .export _UpdateSpritesPpuSpriteIndex_ = UpdateSpritesPpuSpriteIndex

    ; The palette index of the meta sprite.
    UpdateMetaSpritePalette: .res 1
    .export _UpdateMetaSpritePalette_ = UpdateMetaSpritePalette

    ; The center x position of the meta sprite.
    UpdateMetaSpriteX: .res 2
    .export _UpdateMetaSpriteX_ = UpdateMetaSpriteX

    ; The bottom y position of the meta sprite.
    UpdateMetaSpriteY: .res 1
    .export _UpdateMetaSpriteY_ = UpdateMetaSpriteY

    ; The sprite attributes.
    Attributes: .res 1

    ; The attributes value about
    ; mirroring the sprite.
    MirrorAttributes: .res 1

    ; Counters to check the number of drawn tiles.
    XTileCounter: .res 1
    YTileCounter: .res 1

    ; The height of the meta sprite
    ; in tiles.
    TileHeight: .res 1

    ; The X and Y position
    ; of a single sprite.
    AbsoluteX: .res 2
    AbsoluteY: .res 1

    ; The leftmost x position
    ; of the meta sprite.
    RelativeX: .res 2

    ; The x position of the meta sprite
    ; when it is mirrored.
    PossiblyMirroredRelativeX: .res 2

.segment "CODE"

.export _UpdateMetaSprite_ = UpdateMetaSprite

    ; The attributes, which are
    ; stored in A, are OR-connected
    ; with the palette index.
    ; and saved to the variable.
    ORA UpdateMetaSpritePalette
    STA Attributes

    ; Furthermore, the mirror attributes
    ; are extracted and saved as well.
    ; This is done because we need them
    ; for a later check.
    AND #%01000000
    STA MirrorAttributes

    ; The width of the current meta sprite,
    ; counted in tiles, not in pixels.
    ; That's the counter value for the X loop,
    ; i.e. the outer loop.
    LDA #CharacterTileWidth
    STA XTileCounter

    ; The absolute X position is in the center
    ; of the meta sprite.
    ; The relative X position gets moved
    ; from the center to the left,
    ; so that this value points to the
    ; leftmost position of the meta sprite.
    LDA #<(-CharacterWidth / 2)
    STA RelativeX
    LDA #<-1
    STA RelativeX + 1

    ; The index of the meta sprite array,
    ; starting at the position of the const pointer.
    LDY #0

    ; The height, counted in tiles.
    LDA (ConstPointer), Y
    STA TileHeight

    ; The absolute Y position is at the bottom
    ; of the meta sprite.
    ; So, it is moved eight pixels to the top,
    ; so that the tiles' bottoms are actually
    ; at the desired position.
    LDA UpdateMetaSpriteY
    SBC #8
    STA UpdateMetaSpriteY

    ; Some characters cannot be drawn
    ; with their feet in the bottom position.
    ; For these meta sprites,
    ; the offset value is added to the Y position,
    ; so that they're still in the correct position.
    LDA UpdateMetaSpriteY
    ADC (ConstPointer), Y
    STA UpdateMetaSpriteY

    ; The index of the PPU sprites
    ; that are written next.
    LDX UpdateSpritesPpuSpriteIndex

    ; The outer loop: All rows are drawn
    ; from left to right.

    ; The height in tiles becomes
    ; the loop counter.
    LDA TileHeight
    STA YTileCounter

    ; The absolute Y value is set
    ; to its starting position.
    LDA UpdateMetaSpriteY
    STA AbsoluteY

    ; If the meta sprite shall be mirrored,
    ; we have to manipulate the X position.
    LDA MirrorAttributes
    BEQ @noMirroring

    ; The relative X position gets inverted
    ; and subtracted with 7.
    ; This way, it has the correct value
    ; to render the tile at the opposite
    ; of the meta sprite's center.
    ; The new value is stored in a separate variable.
    LDA RelativeX
    EOR #%11111111
    SBC #7
    STA PossiblyMirroredRelativeX
    LDA RelativeX + 1
    EOR #%11111111
    SBC #0
    STA PossiblyMirroredRelativeX + 1

    JMP @endMirroring


    ; If no mirroring is done,
    ; the value is simply copied
    ; into the new variable.
    LDA RelativeX
    STA PossiblyMirroredRelativeX
    LDA RelativeX + 1
    STA PossiblyMirroredRelativeX + 1


    ; We take the original absolute
    ; centered X position
    ; and add the relative X position to it.
    ; This way we get the actual value
    ; that needs to be used for the rendering.
    LDA UpdateMetaSpriteX
    ADC PossiblyMirroredRelativeX
    STA AbsoluteX
    LDA UpdateMetaSpriteX + 1
    ADC PossiblyMirroredRelativeX + 1
    STA AbsoluteX + 1

    ; The inner loop: Every tile in this column
    ; is rendered from bottom to top.

    ; The tile is read from the meta sprites array
    ; and set to the sprites array.
    LDA (ConstPointer), Y
    STA Sprites + 1, X

    ; If the high byte of x is not 0,
    ; this means this specific sprite
    ; is outside the screen.
    ; In this case, the rendering is skipped.
    ; It doesn't matter that the tile value
    ; in the sprites array is already written.
    ; As long as UpdateSpritesPpuSpriteIndex
    ; isn't incremented, the ClearSprites function
    ; will make sure that all unused sprites
    ; are put outside the screen in the end.
    LDA AbsoluteX + 1
    BNE @endRendering

    ; For y, we check whether
    ; the sprite would be located
    ; inside the status bar.
    ; If not, the sprite is rendered.
    ; The status bar y positions
    ; are treated as out-of-screen,
    ; so that y variables can be
    ; one byte instead of two.
    LDA AbsoluteY
    CMP #LevelAbsoluteTopRow * 8 - 1
    BCS @render

    ; If the sprite is inside
    ; the status bar, then we
    ; check whether we're even
    ; in a level, i.e. whether
    ; the system sprites are
    ; not off-screen. Because
    ; in text screens, this
    ; rule doesn't apply.
    LDA Sprites + 0
    CMP #SpriteOffscreenY
    BNE @endRendering


    ; If the sprite is on screen,
    ; we set the other sprite values.

    ; The Y position is written
    ; to the sprites array.
    LDA AbsoluteY
    STA Sprites, X

    ; The X increment for
    ; reading the tile
    ; which was done earlier.

    ; The attributes are read
    ; and then written to the
    ; sprites array.
    LDA Attributes
    STA Sprites, X

    ; The low byte of the X position
    ; is written to the sprites array.
    LDA AbsoluteX
    STA Sprites, X

    ; UpdateSpritesPpuSpriteIndex gets
    ; the value of the X register.
    ; This value corresponds with the four bytes
    ; that we have written to the sprites array.
    ; The PPU will render the current sprite
    ; on the screen.
    STX UpdateSpritesPpuSpriteIndex


    ; If the Y counter is 0,
    ; the inner loop isn't repeated anymore
    ; and all of the loop preparation
    ; is skipped.
    DEC YTileCounter
    BEQ @noLoopY

    ; For the next loop,
    ; the Y position is decremented
    ; with 8, i.e. one tile height.
    LDA AbsoluteY
    SBC #8
    STA AbsoluteY

    ; The inner loop is repeated.
    JMP @loopY


    ; If the X counter is 0,
    ; the function ends.
    ; Otherwise, the outer loop
    ; is repeated.
    DEC XTileCounter
    BEQ @noLoopX

    ; For the next loop,
    ; the X position is incremented
    ; with 8, i.e. one tile width.
    LDA RelativeX
    ADC #8
    STA RelativeX
    LDA RelativeX + 1
    ADC #0
    STA RelativeX + 1

    ; The outer loop is repeated.
    JMP @loopX


My game "City Trouble":
Gameplay video:
Download (ROM, manual, artworks):
Post Reply