How to prevent projectile and "medusa head" slowdowns

FrankenGraphics

New member
If you're doing an action game, this modification is for you. There are even better ways, but this way is quick and simple.

Projectiles and medusa heads are just popular examples of enemy actors that could benefit from this mod. ANYTHING that doesn't benefit from tile collision should really go on this list. It is also beneficial to include stationary enemies on the list (and they wouldn't even need to update positions .. they can actually skip a lot of code in another routine (HandleUpdateObjects.asm) this tutorial doesn't cover, but let's start here).

What objects should go on the exception list is something you need to decide game-for-game and may change as your design is worked on. In the example, we're only making an exception for the single projectile type.


1) Identify what game object(s) you're using as projectiles. In my case, i've set the object that is called "projectile" by default in the adventure module as my monster projectile, whereas it was originally set to 1 (which would be the one named "melee" in the gui). So, counting from the player object, which is object type 0, my monster projectile is in other words id #3.

2) Search and open HandleTileCollison.asm

3) Right after line 1 (the label), press enter and copy the following:
Code:
	lda Object_type,x
	cmp #3
	bne +
		jsr updateHorizontalPosition
		jsr updateVerticalPosition
		rts
		+

note that you have to edit that #3 if you have another or several objects you wish to be excempt from tile collisions.

3) Save a copy. In project settings, under the script settings link to the new copy.

4) Done. No more slowdowns... up to a limit of bullets, of course. I just tested 4 bullet generators in my quick test and that worked just fine.

Some precautions and notes:
-The projectile types included in the exception will no longer be able to react against solids. If this is important to your game design, you can insert a custom 1-point collision subroutine into the exception which would be loads leaner than the "do everything" 6-point collision that is currently used for all objects and still be accurate enough for small projectiles and the like. That's for another day. Usually you want player bullets to be wall blockable...

-If you swap around your objects, be sure to swap around the cmp arguments too to match.

-Even better would be to remove all the reduntant JSR / RTS calls done by HandleUpdateObjects and its various subroutines. Those add up to quite a bit of unnecessary overhead, especially since they're repeated once for every active object. This would increase the speed of collision checks for all objects a bit. JSR / RTS pairs are something you want a minimum of in any often-repeated code.

EDIT:
In case anyone is wondering, i was able to get from 4-5 objects (player included) without slowdown to 7-8 objects (player included) without slowdown on average. Both cases including a portion of aimed physics. So it's a significant improvement, but could still be better. Your mileage will vary depending on your constellations of objects that need/doesn't need tile collisions.
 

FrankenGraphics

New member
There's no distinction! Object_type #$10 is actually monster type 0! I wasn't even aware there was a monster_type variable..

So for example, if you want all monsters above and/or below some range of ID:s to be non-tile collidable, you can use these statements to define such a range:

greater than (>) :
beq +
bcs myLabel
+

greater than OR equal to (>=) :
bcs myLabel

less than (<) :
bcc myLabel

less than OR equal to (<=) :
beq myLabel
bcc myLabel


In my case, i've ordered all the "free flying" (and also stationary) monsters (ie ones that wouldn't need to be wall blockable or use stairs or whatever in any case) together so i only need to specify two exception clauses: one specific for my enemy projectile, and one that simply implies that only enemies and objects below #$26 are elegible for tile collision:

Code:
	lda Object_type,x
	cmp #3
	bne +
		JSR updateHorizontalPosition
		JSR updateVerticalPosition
		rts
		+
	cmp #$26 ;let's filter "above solid-flying monsters" from tile collision 
	bcc +
		JSR updateHorizontalPosition
		JSR updateVerticalPosition
		rts
		+

I could add one for the stationaries that doesn't even update positions if needed to, but at that point i might as well attempt a more complete object handling overhaul.. something i probably won't have time for until after the project is completed anyway
 

ZeGGamer1

New member
So, basically, any monster that appears above the 26 monsters in the list will have collision?

Or any monster below the 26 monsters will not have collision?
 

FrankenGraphics

New member
The opposite - any monsters equal to or above $26 will ignore collision altogether in my case.

also, it's not 26 monsters in two ways:
-$26 is in hexadecimal. so the line goes at object 38 counted in decimal.
-Objects 0-15 ($0-$f) are "game objects", ie player, death object, etc. Subtract $10 to get to the actual monster count in your "monster drawer" in the NESmaker tool GUI.
 

FrankenGraphics

New member
It's also easy to get confused (i get confused all the time even though i've memorized this) by the negative nature of branches. The way they're often used is more like "if this is true, then DON'T do this thing - skip ahead instead" or "if this is false, do this thing instead of skipping ahead", where a simpler IF/ELSE clause in BASIC or some other more human-friendly language would read as "if this is true, then yeah do this thing, else, do that other thing".

Whenever i've caused a blatant bug, the first thing i check for is if i got the branch condition the wrong way around - because often, that's it.
 

AllDarnDavey

Active member
This is cool. So excluding it from the more expensive collision checks doesn't break player hit-box damage checks? That's good to know.

FrankenGraphics said:
...the first thing i check for is if i got the branch condition the wrong way around - because often, that's it.

I continue to get branches wrong almost every time as well.
 

FrankenGraphics

New member
That's right - object vs object collision is a whole other collision detection phase.

the object vs object phase is much leaner because only the player object and the player melee checks against other objects.



At some point i might attempt a better mod where hit boxes are offset/radius based and you simply add object position, hitbox anchor offset and hitbox radius together on each axis for collision detection, which could potentially cut the total collision time down to an approximate third; very naïvely/optimistically "calculated". Divide that difference by my ineptitude at writing cycle efficient code and we'll get to something mroe realistic :p Even in the best case scenario, this is NOT to say that you can stuff loads more of objects - remember that the rest of the maingame loop needs to be executed too and takes its toll. I suspect this will break GUI editing of hit boxes, though.

In between i'm going to refactor the existing collision engine to not use as many code jumps. It'll have the benefit on not needing modifications to other parts of the code, and no risk of making the GUI losing touch. But it'll take some time before i can get to it; the plate is full until maybe 3rd week in april.
 
Thanks for the tip!

Not sure I did it the most efficient way, but I got a little extra performance out of the change.
I separated objects into 3 categories: projectiles get 1-point collision, the player gets 6-point collision, all other objects get 4-point collision.
 

FrankenGraphics

New member
Cool! How did you go about implementing 1- and 4-point collisions?

Another big improvement that could be had would be to only check for tiles every 16th pixel position on each axis. the foundation of this is simple

Code:
lda Object_x_hi,x 
and #15 
bne +       ;this filters out any x position that isn't divisible by 16
	jsr myCheckHorzCollisionRoutine
	+

This doesn't work with NESmaker without some extensive modifications, though. From what i've gathered, NESmaker collision statuses are cleared at the start of the cleared, and so the collision needs to be continuously flagged right after regardless screen position. So the quite dramatic time reduction that could be had by only checking for collisions when on grid doesn't work, since the game relies on being checked at every position or else it is considered a noncollision, which is how the walkable tile script contains no code whatsoever and still works. There may also be trouble with priorities if colliding with severaly types of tiles. Not sure yet.
 

WillElm

New member
Thanks this works really well. I have a question though. Actually, it's more of an observation. When I add my enemy bullet and player melee object, it seems to help more than if I just put in the enemy projectile. But when I add more, the effect seems to be negated and performance worsens. So I guess you can only have one or two objects, otherwise, you have to define ranges like described in this thread.

Is there an easy way to reorder objects? Or do you have to just do it manually?

here's what my "exclusion list" looks like. I commented out all the other objects because I noticed the game was slowing down. So many of my objects don't need collision so I expect the code found later in the thread should help more. Thanks again.

Code:
HandleTileCollision:
	lda Object_type,x
	cmp #1  ; player melee
;	cmp #9  ; monster death
;	cmp #14 
;	cmp #17 
	cmp #18 ; enemy bullet
;	cmp #19
;	cmp #20
;	cmp #21
;	cmp #22
;	cmp #23 ; enemy explosion
;	cmp #27
	bne +
		jsr updateHorizontalPosition
		jsr updateVerticalPosition
		rts
		+
 

Kasumi

New member
I am not sure what you mean by collision statuses don't latch, or pulled low. Hardware terms?

You almost certainly don't want use that AND to try to avoid collision checks, though, since it only works if you only ever move 1 pixel a frame. If I'm a pixel to the left of a boundary and move two pixels that frame, I skip the check and am inside the wall. If you want to do something like this, you can check for a bit flip (did adding how many pixels I moved make bit #%00010000 different than before I moved?) which is a bit safer, but would break for collision state changes. (Like a not solid tile becoming solid or vice versa)
 

Kasumi

New member
WillElm, your code currently boils down to this:
Code:
HandleTileCollision:
	lda Object_type,x
	cmp #1  ; player melee
	cmp #18 ; enemy bullet
	bne +
		jsr updateHorizontalPosition
		jsr updateVerticalPosition
		rts
		+
Branches are based on the "status flags" and each instruction updates the status flags, losing what they used to be. So the result of the cmp #1 will never be used, because the result from cmp #18 overwrites it before the branch. You need a branch for every cmp.
Code:
HandleTileCollision:
	lda Object_type,x
	cmp #1  ; player melee
        beq runOnlyMove;If object type is equal to player melee, go to the runOnlyMove
	cmp #18 ; enemy bullet
	bne +;If the object is not equal to player bullet, do the rest of the routine
runOnlyMove:
		jsr updateHorizontalPosition
		jsr updateVerticalPosition
		rts
		+
One other thing, if you change this:
Code:
		jsr updateVerticalPosition
		rts
to this
Code:
		jmp updateVerticalPosition
You save 9 cycles. It's called a tail call. https://wiki.nesdev.com/w/index.php/6502_assembly_optimisations#Avoid_a_jsr_.2B_rts_chain
 

FrankenGraphics

New member
@willElm:
But when I add more, the effect seems to be negated and performance worsens. So I guess you can only have one or two objects, otherwise, you have to define ranges like described in this thread.
Yes, well beyond what kasumi wrote, the principle is still correct - keep in mind that you run through an iteration of all these checks for each active object, so if you have seven active objects, that list is (partially) run through seven times. You want to get out of checking as early as possible. Each check leading up to the important one for the object in question is a waste of cycles. In other words, keep the checks as few as possible (and preferably the most quantitatively/likely occurring case first to make the most of it), or else it *eventually* starts becoming another performance problem in the same fashion of what we're trying to avoid.. except collisions will likely always take more time than checks for avoiding them. It's a limitation you need to deal with a little bit at least in lieu of a system built around a few tiers of object complexity.

@kasumi
Yeah sorry, those are hardware terms... this has nothing to do with hardware, of course. clearing, setting and resetting would've been better word choices.

I learned the AND trick from metroid, which admittedly has more handling for getting an object outside a solid area; including horizontal push-out (also what SMB does - pushing you out until you're free) and vertical jump-through. I don't recall getting pushed back out of or needing to jump out of the ground despite falling acceleration moving a lot quicker than a pixel though... i need to go back and study what they did there.

I was under the impression this is a common technique.

Edit: just checked the disassembly and nope, metroid actually doesn't have horizontal push-out. I remembered that bit wrong.
 
Top Bottom