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.
Blades of the Lotus - Platformer
Moderator: Moderators
Re: Blades of the Lotus - Platformer
If the tiles are centered around the origin, I think the calculation is simply:
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 , but for some reason this makes more sense in my head.
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.
Code: Select all
new_x_offset = (x_offset + 8) * -1;
render_x_pos = meta_x + new_x_offset;
Maybe that's just another way of saying what Doug suggested , but for some reason this makes more sense in my head.
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.
Re: Blades of the Lotus - Platformer
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.Goose2k wrote: ↑Sat May 07, 2022 10:12 pmCode: Select all
new_x_offset = (x_offset + 8) * -1; render_x_pos = meta_x + new_x_offset;
Re: Blades of the Lotus - Platformer
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:
Assembly code:
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); \
}
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"
UpdateMetaSprite:
.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
INY
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.
SEC
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.
CLC
LDA UpdateMetaSpriteY
ADC (ConstPointer), Y
INY
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.
@loopX:
; 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.
SEC
LDA RelativeX
EOR #%11111111
SBC #7
STA PossiblyMirroredRelativeX
LDA RelativeX + 1
EOR #%11111111
SBC #0
STA PossiblyMirroredRelativeX + 1
JMP @endMirroring
@noMirroring:
; If no mirroring is done,
; the value is simply copied
; into the new variable.
LDA RelativeX
STA PossiblyMirroredRelativeX
LDA RelativeX + 1
STA PossiblyMirroredRelativeX + 1
@endMirroring:
; 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.
CLC
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.
@loopY:
; The tile is read from the meta sprites array
; and set to the sprites array.
LDA (ConstPointer), Y
INY
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
@render:
; 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
INX
; The X increment for
; reading the tile
; which was done earlier.
INX
; The attributes are read
; and then written to the
; sprites array.
LDA Attributes
STA Sprites, X
INX
; The low byte of the X position
; is written to the sprites array.
LDA AbsoluteX
STA Sprites, X
INX
; 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
@endRendering:
; 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.
SEC
LDA AbsoluteY
SBC #8
STA AbsoluteY
; The inner loop is repeated.
JMP @loopY
@noLoopY:
; 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.
CLC
LDA RelativeX
ADC #8
STA RelativeX
LDA RelativeX + 1
ADC #0
STA RelativeX + 1
; The outer loop is repeated.
JMP @loopX
@noLoopX:
RTS
My game "City Trouble":
Gameplay video: https://youtu.be/Eee0yurkIW4
Download (ROM, manual, artworks): http://www.denny-r-walter.de/city.html
Gameplay video: https://youtu.be/Eee0yurkIW4
Download (ROM, manual, artworks): http://www.denny-r-walter.de/city.html
Re: Blades of the Lotus - Platformer
UPDATE: I could see the writing on the wall with this one, and wasn't going to be able to deliver it on time for the competition. A few weeks ago though, I pivoted to a new project MINEKART MADNESS, which should be done for the compo!