How to achieve decent 16 bit physics in CA65 ASM [solved]

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

Post Reply
whoshotdk
Posts: 4
Joined: Thu Oct 13, 2022 7:52 am

How to achieve decent 16 bit physics in CA65 ASM [solved]

Post by whoshotdk »

Hello!

I am only a few days into assembly programming and am messing about trying to create 'lunar lander' like physics.

I've achieved something that works, but it feels janky/sticky. Considering the point of the game is to land softly, I am finding it difficult to do that when testing the game.

There is actually nothing to land on yet, but its easy to see how difficult it would be to land if you play.

The ROM is here: https://whoshotdk.co.uk/project.nes (D-pad controls)

I've read about using two bytes for both position and velocity, one for the integer part and one for the fractional part. I thank the nes-god-king Rainwarrior for that idea. I have attempted to implement that. I'm fairly sure I've got it wrong, probably in the 'euler integration (lol)' part of the code, where I'm attempting to do a 16-bit addition.

The main game loop is posted below.

I'd really appreciate a clear explanation of how to do '16-bit' position/velocity physics. As I could not find that on the 'net already, I'm sure it'd be appreciated by others too! Unless my Google-foo has failed me.

Thanks!
Dave

Code: Select all

; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
; these are the constants im using

MAX_VELOCITY_X = $20
MAX_VELOCITY_Y = $20

PLAYER_THRUSTER_X = $20
PLAYER_THRUSTER_Y = $20

GRAVITY = $10

; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
; zero page segment

playerX:				.res 2				; high = integer, low = fraction
playerY:				.res 2				; high = integer, low = fraction

playerVX:				.res 2				; high = integer, low = fraction
playerVY:				.res 2				; high = integer, low = fraction

; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
; code segment, main loop...

Update:

	; copy to sprites

	LDA playerX
	STA $0203

	LDA playerY
	STA $0200

	; player input

	JSR ReadInput

	LDA input
	AND #BUTTON_LEFT
	BEQ :+								; skip if button not pressed
	LDA playerVX + 1						; dec fraction byte
	SEC
	SBC #PLAYER_THRUSTER_X
	STA playerVX + 1
	BCS :+								; skip if fraction byte overflowed
	LDA playerVX							; dec high byte
	SEC
	SBC #$01
	STA playerVX
	:

	LDA input
	AND #BUTTON_RIGHT
	BEQ :+								; skip if button not pressed
	LDA playerVX + 1						; inc fraction byte
	CLC
	ADC #PLAYER_THRUSTER_X
	STA playerVX + 1
	BCC :+								; skip if fraction byte overflowed
	LDA playerVX							; inc high byte
	CLC
	ADC #$01
	STA playerVX
	:

	LDA input
	AND #BUTTON_UP
	BEQ :+								; skip if button not pressed
	LDA playerVY + 1						; dec fraction byte
	SEC
	SBC #PLAYER_THRUSTER_Y
	STA playerVY + 1
	BCS :+								; skip if fraction byte overflowed
	LDA playerVY							; dec high byte
	SEC
	SBC #$01
	STA playerVY
	:

	LDA input
	AND #BUTTON_DOWN
	BEQ :+								; skip if button not pressed
	LDA playerVY + 1						; inc fraction byte
	CLC
	ADC #PLAYER_THRUSTER_Y
	STA playerVY + 1
	BCC :+								; skip if fraction byte overflowed
	LDA playerVY							; inc high byte
	CLC
	ADC #$01
	STA playerVY
	:

	; gravity

	LDA playerVY + 1
	CLC
	ADC #GRAVITY
	STA playerVY + 1
	BCC :+								; skip if fraction byte overflowed
	LDA playerVY							; inc high byte
	CLC
	ADC #$01
	STA playerVY
	:

	; limit velocity

	; x

	LDA playerVX
	BEQ :++								; skip if velocity = 0
	BPL :+								; skip if velocity > 0
	CMP #($FF - MAX_VELOCITY_X + 1)					; velocity < 0, compare against maximum
	BPL :+								; skip if velocity < maximum
	LDA #($FF - MAX_VELOCITY_X + 1)					; velocity > maximum, clamp it
	STA playerVX
	JMP :++
	:
	CMP #MAX_VELOCITY_X						; velocity > 0, compare against maximum
	BMI :+								; skip if velocity < maximum
	LDA #MAX_VELOCITY_X						; velocity > maximum, clamp it
	STA playerVX
	:

	; y

	LDA playerVY
	BEQ :++								; skip if velocity = 0
	BPL :+								; skip if velocity > 0
	CMP #($FF - MAX_VELOCITY_Y + 1)					; velocity < 0, compare against maximum
	BPL :+								; skip if velocity < maximum
	LDA #($FF - MAX_VELOCITY_Y + 1)					; velocity > maximum, clamp it
	STA playerVY
	JMP :++
	:
	CMP #MAX_VELOCITY_Y						; velocity > 0, compare against maximum
	BMI :+								; skip if velocity < maximum
	LDA #MAX_VELOCITY_Y						; velocity > maximum, clamp it
	STA playerVY
	:

	; euler integration

	LDA playerVX + 1						; velocity x fraction
	CLC
	ADC playerX + 1
	STA playerX + 1

	LDA playerVX							; velocity x high
	CLC
	ADC playerX
	STA playerX

	LDA playerVY + 1						; velocity y fraction
	CLC
	ADC playerY + 1
	STA playerY + 1

	LDA playerVY							; velocity y high
	CLC
	ADC playerY
	STA playerY

	; debug

	LDA playerVY
	STA $F0

	LDA playerVY + 1
	STA $F1

	; iterate

	JSR WaitVBlank
	JMP Update
Last edited by whoshotdk on Fri Oct 21, 2022 4:33 am, edited 1 time in total.
Fiskbit
Posts: 891
Joined: Sat Nov 18, 2017 9:15 pm

Re: How to achieve decent 16 bit physics in CA65 ASM

Post by Fiskbit »

Indeed, the big issue I see in this code is that you're not using carry as intended. When you do multi-byte adds or subtracts, you clc or sec before operating on the lowest byte, but then you need to not touch carry at all for the higher bytes because the carry value from the lower bytes needs to carry into the higher ones. So for example, in the following code:

Code: Select all

	LDA playerVX + 1						; velocity x fraction
	CLC
	ADC playerX + 1
	STA playerX + 1

	LDA playerVX							; velocity x high
	CLC
	ADC playerX
	STA playerX
you shouldn't be doing CLC on the higher byte. It should look like this:

Code: Select all

	LDA playerVX + 1						; velocity x fraction
	CLC
	ADC playerX + 1
	STA playerX + 1

	LDA playerVX							; velocity x high
	ADC playerX
	STA playerX
Your code as it is isn't carrying the 1 like it should be, which means the fractional velocity can't affect the integer part of the position; it's only when thrust causes the fractional velocity to increment the integer velocity that the position is able to change.


Your velocity calculations don't look wrong per se, but you can take advantage of carry there, too. For example, this code:

Code: Select all

	LDA input
	AND #BUTTON_LEFT
	BEQ :+								; skip if button not pressed
	LDA playerVX + 1						; dec fraction byte
	SEC
	SBC #PLAYER_THRUSTER_X
	STA playerVX + 1
	BCS :+								; skip if fraction byte overflowed
	LDA playerVX							; dec high byte
	SEC
	SBC #$01
	STA playerVX
	:
can turn into this:

Code: Select all

	LDA input
	AND #BUTTON_LEFT
	BEQ :+								; skip if button not pressed
	LDA playerVX + 1						; dec fraction byte
	SEC
	SBC #PLAYER_THRUSTER_X
	STA playerVX + 1
	LDA playerVX							; dec high byte
	SBC #$00
	STA playerVX
	:
or, because you know the upper part of thrust is always 0, you can do it like you did before, but with a decrement to save some bytes and cycles:

Code: Select all

	LDA input
	AND #BUTTON_LEFT
	BEQ :+								; skip if button not pressed
	LDA playerVX + 1						; dec fraction byte
	SEC
	SBC #PLAYER_THRUSTER_X
	STA playerVX + 1
	BCS :+								; skip if fraction byte overflowed
	DEC playerVX							; dec high byte
	:

Edit: Also, one thing you should be doing in your ROM is clearing memory on boot. You can't rely on CPU or PPU RAM being 0 on real hardware, and running this ROM in Mesen with randomized RAM shows garbage in the background because the nametables are uninitialized.
calima
Posts: 1745
Joined: Tue Oct 06, 2015 10:16 am

Re: How to achieve decent 16 bit physics in CA65 ASM

Post by calima »

It's also weird that you're using big endian. Most code out there is LE, be careful you don't get confused which byte is which.
User avatar
rainwarrior
Posts: 8732
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: How to achieve decent 16 bit physics in CA65 ASM

Post by rainwarrior »

For 16-bit operations on a 6502, this was continually my source of reference. It lays out all the common patterns of use:
http://www.6502.org/tutorials/compare_beyond.html

The ones I used a lot ended up just becoming memorized and automatic, but I still consult this article when doing less common things.

Usually the 16-bit version of something is very similar to an 8-bit version, but having to load and store a bit more, not being able to keep everything in the A register. For a lot of purposes the carry flag is the glue that connects the low byte operation to the high byte.

Code: Select all

; 8-bit q = o + p
lda o
clc
adc p
sta q

; 16-bit q = o + p
lda o+0
clc
adc p+0
sta q+0
; high byte is the same but use the carry result now
lda o+1
adc p+1
sta q+1
whoshotdk
Posts: 4
Joined: Thu Oct 13, 2022 7:52 am

Re: How to achieve decent 16 bit physics in CA65 ASM

Post by whoshotdk »

@Fiskbit

I cannot thank you enough! First, for pointing out my mistake in an easy-to-understand manner with example code and second, additional debugging re improving it and also about the nametables.

To be honest this is just a test rom, separate from my game, so it has nothing but what I need to test the physics. But I will now remember to check my ROM in multiple emulators to be sure values are set correctly. Thank you!

@Calima

I just never got the 'hang' of LE. BE requires a lot less 'pause and think' time for me :) I shall take your advice into consideration though; its probably best for me to stick with one or the other and not mix them up.

@Rainwarrior

The man himself! Your reputation precedes you - I've already seen you about here and on YouTube, you are an inspiration. Thank you for the useful link, I've bookmarked that one now.

Thank You to you all, my first post here and I have three replies and a solution to my problem. Put that in your pipe and smoke it, StackOverflow!

Wishing you all the best,
Dave
Fiskbit
Posts: 891
Joined: Sat Nov 18, 2017 9:15 pm

Re: How to achieve decent 16 bit physics in CA65 ASM [solved]

Post by Fiskbit »

Glad this was helpful!

I definitely agree about using little endian instead of big endian, since pointers on this system are little endian, and thus most pointer math you do will have to be little endian. (The annoying exception is that the address written to the PPU is big endian.)

That said, if you make a game with a proper object system (to support enemies and potentially more than one player), you'll find that it's best to have one array per object variable. That means your multi-byte positions and velocities get split up into multiple arrays, such that you'll need to name each component rather than using offsets from a base variable address like you're using here.
whoshotdk
Posts: 4
Joined: Thu Oct 13, 2022 7:52 am

Re: How to achieve decent 16 bit physics in CA65 ASM [solved]

Post by whoshotdk »

Hi Fiskbit

Thanks for the further advice. I have in fact attempted to create some kind of entity management system, using CA65 structs;

Code: Select all


MAX_ENTITIES = 25

.struct Entity

	xpos	.res 2
	ypos	.res 2
	xvel	.res 2
	yvel	.res 2
	type	.res 1
	data	.res 1
.endstruct
With a corresponding variable in BSS (would have been nice to use ZP, but I need more than 256 bytes):

Code: Select all

entities:				.res .sizeof(Entity) * MAX_ENTITIES
However, I am quickly realising that this is not ideal - due to the size of the 'struct', when doing a simple indexed lookup like so;

Code: Select all

LDA entities+Entity::xvel, y
I'm limited to how many entities I can offset with a single register (y).

I am currently mulling over the idea of implementing a nested loop (like when you copy more than 256 bytes to a PPU nametable). Not sure if that is the way to go though.

I don't know if I am understanding your suggestion correctly - I take it to mean that I'd have separate 'arrays' for the structure members rather than a single array of structs. I.e an array that stores all the velocity x's and I would index into that with an entity offset. Then another for velocity y e.t.c.

EDIT: After re-reading, you were pretty clear on the matter actually!
EDIT 2: Oh! And using seperate arrays would allow me to index by 1, not by the size of the struct. That'd be incredibly useful!

I believe that would be more efficient too? It reminds me of the way ECS attempts to organise memory so that related data is stored consecutively.

Apologies if all what I have said is wrong or foolish. I am still getting to grips with this low level stuff!

Dave
Fiskbit
Posts: 891
Joined: Sat Nov 18, 2017 9:15 pm

Re: How to achieve decent 16 bit physics in CA65 ASM [solved]

Post by Fiskbit »

Right, arrays of structs are a very bad fit for the 6502. Generally the way you'd make them work is by adjusting a pointer to point at the start of the struct and then having Y index into it with (zp),Y to get the desired variable. This is a slow process and using arrays is much better. With arrays, your variables would look like this:

Code: Select all

MAX_ENTITIES = 25
xpos:     .res MAX_ENTITIES
xpos_sub: .res MAX_ENTITIES
ypos:     .res MAX_ENTITIES
ypos_sub: .res MAX_ENTITIES
xvel:     .res MAX_ENTITIES
xvel_sub: .res MAX_ENTITIES
yvel:     .res MAX_ENTITIES
yvel_sub: .res MAX_ENTITIES
type:     .res MAX_ENTITIES
data:     .res MAX_ENTITIES
Then you can have the entity index in X or Y and load from any of these just by loading from the address,index.

Note that 25 is a pretty large number of entities and you'll likely start lagging (exceeding your per-frame CPU cycle budget) before that.
whoshotdk
Posts: 4
Joined: Thu Oct 13, 2022 7:52 am

Re: How to achieve decent 16 bit physics in CA65 ASM [solved]

Post by whoshotdk »

Thanks again Fiskbit.

I was being a bit naive when it comes to the number of entities; I assumed that at the very least I'd be able to run 64 of them, just because that's the sprite limit (I realise that an entity does not necessarily equal a sprite).

Having thought about it, I assume the maximum viable number of entities is based on the speed of the code and available memory. I'm sure that's obvious to most but sometimes my common sense is found lacking ;-)

Fortunately for me, this first game I am creating requires only several entities anyway.

Have a great day!
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: How to achieve decent 16 bit physics in CA65 ASM [solved]

Post by tokumaru »

It's true that an NES game with 20+ entities active at all times will have a hard time maintaining a smooth frame rate, but I think it's still a good idea to have a decent number of object slots so you're prepared for more intense spawning/despawning situations. I personally have been using 32 slots.

In games with lots of dynamic content, such as projectiles, explosions and such, it's easy to end up with a large number of active objects during short periods. Also, in games that scroll, entities usually spawn and despawn off-screen, so the program needs to accommodate more than just what's visible. But that doesn't mean that the CPU is always overworked, since projectiles and effects tend to be much simpler to process than entities with physics and collisions.
Post Reply