Making an enemy move along surfaces

Discussion of hardware and software development for Super NES and Super Famicom.

Moderator: Moderators

Forum rules
  • For making cartridges of your Super NES games, see Reproduction.
Post Reply
imamelia
Posts: 14
Joined: Wed Sep 01, 2010 12:17 am

Making an enemy move along surfaces

Post by imamelia »

I'm trying to write code for making an enemy follow along all surfaces, including ground, ceilings, walls, and both ground and ceiling slopes. Unfortunately, no matter what I do, I can't quite get it to work right, and I've been trying for months. The only data I have available for what surfaces it's touching or near is what block numbers are nearby, though I can use that to look up what surface type it is based on a table index. I haven't even gotten as far as testing it with slopes; I can't even get the basic straight surface interaction to work. Here is the code that I currently have:

Code: Select all

; CurrentSurface: What surface the sprite is currently on.  Same values as the ones listed in the tables.  $FF = in air.
; Speed: Current speed, unsigned 4.4 ($10 = move 1 pixel per frame).
; Direction: Current movement direction.  0 = clockwise, 1 = counterclockwise.
; TransitionTimer: Timer for transitions.  Is decremented by the current speed value.
; NextSurface: Surface to change to when the transition timer runs out.
; BlockedStatus: Which sides the sprite is blocked on, asb-udlr (udlr = primary foreground above, below, left, right; asb = secondary interactive layer above, side, below)
; $00-$0F, $8A-$8F: Scratch RAM.
; Scratch1, Scratch2: Longer-term scratch RAM that isn't used by any subroutines.
; $005000 etc.: Breakpoints.

; checked block position diagrams; C = current position, 1, 2, 3 = position of first, second, and third blocks to check
; C 2		2 C			1 3		3 1			2 1		C 3			3 C		1 2
; 3 1		1 3			2 C		C 2			C 3		2 1			1 2		3 C

; data for block checking
; 3 blocks per surface, X and Y offsets; 2 directions
BlockCheckData:
; 00: ground
	dw $0010,$0010 : dw $0010,$0000 : dw $0000,$0010
	dw $FFFF,$0010 : dw $FFFF,$0000 : dw $0000,$0010
; 01: ceiling
	dw $FFFF,$FFFF : dw $FFFF,$0000 : dw $0000,$FFFF
	dw $0010,$FFFF : dw $0010,$0000 : dw $0000,$FFFF
; 02: outer left edge
	dw $0010,$FFFF : dw $0000,$FFFF : dw $0010,$0000
	dw $0010,$0010 : dw $0000,$0010 : dw $0010,$0000
; 03: outer right edge
	dw $FFFF,$0010 : dw $0000,$0010 : dw $FFFF,$0000
	dw $FFFF,$FFFF : dw $0000,$FFFF : dw $FFFF,$0000
; 04: steep slope left
; 05: steep slope right
; 06: normal slope left
; 07: normal slope right
; 08: gradual slope left
; 09: gradual slope right
; 0A: very steep slope left
; 0B: very steep slope right
; 0C: steep slope left, upside-down
; 0D: steep slope right, upside-down
; 0E: normal slope left, upside-down
; 0F: normal slope right, upside-down

; table of values to multiply the speed by for different surfaces, in 4.4 fixed-point - X, Y; counterclockwise, clockwise
SpeedMultiplierTable:
; 00: ground
	db $F0,$00 : db $10,$00
; 01: ceiling
	db $F0,$00 : db $10,$00
; 02: outer left edge
	db $00,$10 : db $00,$F0
; 03: outer right edge
	db $00,$F0 : db $00,$10
; 04: steep slope left
	db $F0,$10 : db $10,$F0
; 05: steep slope right
	db $F0,$F0 : db $10,$10
; 06: normal slope left
	db $F0,$08 : db $10,$F8
; 07: normal slope right
	db $F0,$F8 : db $10,$08
; 08: gradual slope left
	db $F0,$04 : db $10,$FC
; 09: gradual slope right
	db $F0,$FC : db $10,$04
; 0A: very steep slope left
	db $F0,$20 : db $10,$E0
; 0B: very steep slope right
	db $F0,$E0 : db $10,$20
; 0C: steep slope left, upside-down
	db $10,$10 : db $F0,$F0
; 0D: steep slope right, upside-down
	db $10,$F0 : db $F0,$10
; 0E: normal slope left, upside-down
	db $10,$08 : db $F0,$F8
; 0F: normal slope right, upside-down
	db $10,$F8 : db $F0,$08

; what the "next" surface is when going around an outside corner
; ground, ceiling, outer left edge, outer right edge; clockwise, counterclockwise
NewSurfaceAtCorner:
	db $03,$02,$00,$01 : db $02,$03,$01,$00
; what the "next" surface is when going around an inside corner
; ground, ceiling, outer left edge, outer right edge; clockwise, counterclockwise
NewSurfaceAtWall:
	db $02,$03,$01,$00 : db $03,$02,$00,$01



; most of the main code starts here
	LDA Speed,x
	CMP #$11
	BCS .ExtraHighSpeed
.NormalSpeed
	STA Scratch0
	JSR CheckBlocks
	JSR MoveAlongBlocks
	LDA CurrentSurface,x
	BPL .Return
	LDY #$00
	LDA BlockedStatus,x
	BIT #$84
	BNE .HitSurface
	INY
	BIT #$28
	BNE .HitSurface
	INY
	BIT #$41
	BNE .HitSurface
	INY
	BIT #$42
	BEQ .Return
.HitSurface
	STZ CurrentSurface,x
.Return
	RTS
.ExtraHighSpeed
	STA Scratch1
	LDA #$10
	STA Scratch0
.Loop
	JSR CheckBlocks
	JSR MoveAlongBlocks
	LDA Scratch1
	SEC
	SBC #$10
	CMP #$11
	BCC .NormalSpeed
	STA Scratch1
	BRA .Loop

;------------------------------------------------
; check the specified blocks
;------------------------------------------------

; If 1 is solid, check if 2 is a wall and transition to that wall if it is.  If 1 is a slope, transition to that slope.  If 1 is air, check if 2 is air, and if it is, check if 3 is solid, and if it is, transition to corner.  If 2 is a slope, transition to that slope.

CheckBlocks:
; if the timer is currently set, don't check any blocks yet and just continue the same way
	LDA TransitionTimer,x
STA $005004
	BEQ .Continue
; otherwise, decrement it by the speed amount and move on when it's at or below 0
	SEC
	SBC Scratch0
	STA TransitionTimer,x
	BEQ .Continue2
	BCC .Continue2
	RTS
.Continue2
; zero out the timer in case it didn't come out evenly, then set the new state
	STZ TransitionTimer,x
	LDA NextSurface,x
	STA CurrentSurface,x
.Continue
STA $005006
; concatenate the X and Y positions for 16-bit referencing (why is SMW so stupid)
	LDA XPosLo,x
	STA $8A
	LDA XPosHi,x
	STA $8B
	LDA YPosLo,x
	STA $8C
	LDA YPosHi,x
	STA $8D
; first, multiply the current surface type by 24 to get the index to the tables
	LDA CurrentSurface,x
STA $005008
	REP #$20
	AND #$00FF
	ASL #3
	STA $00
	ASL
	ADC $00
; add 12 to the index if it's moving the other direction
	LDY Direction,x
	BEQ .Direction0
	CLC
	ADC #$000C
.Direction0
	REP #$10
	TAY
	STY $8E
STA $00500A
; store the position of the first block to RAM for the Map16 check routine
	LDA $8A
	CLC
	ADC BlockCheckData,y
	STA $00
	LDA $8C
	CLC
	ADC BlockCheckData+2,y
	STA $02
	SEP #$30
; Input: $00-$01 = block X position, $02-$03 = block Y position
	JSL FindBlockNumber
	REP #$30
	STA $04
STA $00500C
; store the offsets for the second block so that they can be used in multiple branches
	LDY $8E
	LDA $8A
	CLC
	ADC BlockCheckData+4,y
	STA $00
	LDA $8C
	CLC
	ADC BlockCheckData+6,y
	STA $02
	LDY $04
	SEP #$20
STA $00500E
; now it's time to check what surface type the first block is
	LDA SurfaceType,y
	SEP #$10
	BMI .Air1
	CMP #$04
	BCS .SetNewSurface
; if the first block is solid, check if the second block is a perpendicular blocking surface, and if it is, transition to that surface; if not, don't change anything
.Solid1
	JSL FindBlockNumber
	REP #$30
	TAY
STA $005010
	SEP #$20
; SurfaceType = lookup table that tells which surface each block is (same values as the current surface type table)
	LDA SurfaceType,y
	STA $00
	SEP #$10
	CMP #$04
	BCS .Return
	LDA Direction,x
	ASL #2
	ORA CurrentSurface,x
	TAY
	LDA NewSurfaceAtWall,y
	LDY #$0F
; surfaces 00-03 all count as solid, while anything else is either air or a slope
.SetNewSurface
	STA NextSurface,x
	TYA
	STA TransitionTimer,x
.Return
	RTS
; if the first block is air, check what the second block is
.Air1
	JSL FindBlockNumber
	REP #$30
	TAY
STA $005012
	SEP #$20
	LDA SurfaceType,y
	SEP #$10
	BMI .Air2
; if the second block is a slope, transition to that slope
	CMP #$04
	LDY #$7F
	BCS .SetNewSurface
	RTS
; if the second block is air, check if the third block is solid, and if it is, transition to the next surface around a corner
.Air2
	REP #$30
	LDY $8E
	LDA $8A
	CLC
	ADC BlockCheckData+8,y
	STA $00
	LDA $8C
	CLC
	ADC BlockCheckData+10,y
	STA $02
	SEP #$30
	JSL FindBlockNumber
	REP #$30
STA $005014
; this time, we need to check the block number/acts-like setting itself
	CMP #$0100
; if it's somehow less than #$0100, it acts like air
	BCC .Air3
; blocks 100-110 are solid only on the top, while 111 and beyond are either completely solid or are slopes
	CMP #$0111
	BCS .Corner3
; if it's on a passable ledge, fall off it
.FallOffLedge
STA $005016
	SEP #$30
	LDA #$FF
	TAY
	BRA .SetNewSurface
; if it's in the air, fall immediately
.Air3
STA $005018
	SEP #$30
	LDA #$FF
	STA CurrentSurface,x
	RTS
; if it's going around a corner, check which surface to transition to based on the current surface and direction
.Corner3
STA $00501A
	SEP #$30
	LDA CurrentSurface,x
	STA $00
	CMP #$04
	BCC .NotOnSlope
; if it's currently on a slope, then count all regular slopes as ground and all ceiling slopes as ceiling
	STZ $00
	CMP #$0C
	BCC .NotOnSlope
	INC $00
.NotOnSlope
	LDA Direction,x
	ASL #2
	ORA $00
	TAY
	LDA NewSurfaceAtCorner,y
	LDY #$FF
	JMP .SetNewSurface

;------------------------------------------------
; movement routine
;------------------------------------------------

MoveAlongBlocks:
STA $005020
	LDY Scratch0
	LDA CurrentSurface,x
	BPL .NotInAir
	JSL UpdatePositionWithGravity
.NotInAir
	CMP #$04
	BCC .MoveStraight
	JMP .OnSlope
.MoveStraight
	CMP #$02
	BCS .OnWall
.OnFloorOrCeiling
	EOR Direction,x
	LSR
	BCS .MoveLeft
.MoveRight
	TYA
	ASL #4
	CLC
	ADC XPosFrac,x
	STA XPosFrac,x
	PHP
	TYA
	LSR #4
	PLP
	ADC XPosLo,x
	STA XPosLo,x
	LDA XPosHi,x
	ADC #$00
	STA XPosHi,x
	RTS
.MoveLeft
	TYA
	EOR #$FF : INC
	TAY
	ASL #4
	CLC
	ADC XPosFrac,x
	STA XPosFrac,x
	PHP
	TYA
	LSR #4
	ORA #$F0
	PLP
	ADC XPosLo,x
	STA XPosLo,x
	LDA XPosHi,x
	ADC #$FF
	STA XPosHi,x
	RTS
.OnWall
	EOR Direction,x
	LSR
	BCS .MoveDown
.MoveUp
	TYA
	EOR #$FF : INC
	TAY
	ASL #4
	CLC
	ADC YPosFrac,x
	STA YPosFrac,x
	PHP
	TYA
	LSR #4
	ORA #$F0
	PLP
	ADC YPosLo,x
	STA YPosLo,x
	LDA YPosHi,x
	ADC #$FF
	STA YPosHi,x
	RTS
.MoveDown
	TYA
	ASL #4
	CLC
	ADC YPosFrac,x
	STA YPosFrac,x
	PHP
	TYA
	LSR #4
	PLP
	ADC YPosLo,x
	STA YPosLo,x
	LDA YPosHi,x
	ADC #$00
	STA YPosHi,x
	RTS
.OnSlope
	LDA XPosLo,x
	STA $00
	LDA XPosHi,x
	STA $01
	LDA YPosLo,x
	AND #$F0
	STA $02
	LDA YPosHi,x
	STA $03
	JSL FindBlockNumber
	REP #$30
; blocks 16E-1D7 are slopes
	CMP #$01D8
	BCS .Return
	CMP #$016E
	BCC .Return
	SBC #$016E
	TAY
; [$82] points to a table that indicates the distance between the top of the block and the top of the slope for each pixel of each slope tile
	LDA [$82],y
	AND #$00FF
	ASL #4
	STA $00
	LDA $94
	AND #$000F
	ORA $00
	TAX
	SEP #$20
	LDA SlopeDataTbl,x
	SEP #$10
	AND #$0F
	ORA $02
	STA YPosLo,x
.Return
	RTS
Yes, the X and Y position high and low bytes are in separate 8-bit tables. It's dumb, but I can't change it because this is for a ROM hack. But I don't know how I'd do it in a homebrew either. In fact, I don't even know how I'd do it in a modern engine, and Google was no help. If I'm going about this task all wrong and there's a much better way to do it, feel free to provide alternative suggestions, or even code; I literally could not pay people to program this for me, so any meaningful help is appreciated.
Oziphantom
Posts: 1350
Joined: Tue Feb 07, 2017 2:03 am

Re: Making an enemy move along surfaces

Post by Oziphantom »

normally you need to have special characters on the corners that you can detect against and then know which way to move next.
So given a facing direction you can then move until you hit X char at which point when you reach X and the next is Air you can then use facing direction + char to get the next direction. You usually then have to do a bunch of offsets when you change face to make the sprites line up with is usually per sprite and direction so you need another table for it.
none
Posts: 111
Joined: Thu Sep 03, 2020 1:09 am

Re: Making an enemy move along surfaces

Post by none »

It's difficult to tell what's going on just from looking at the code.

What is actually going wrong? Can you post a screenshot / gif of what is happening?
imamelia
Posts: 14
Joined: Wed Sep 01, 2010 12:17 am

Re: Making an enemy move along surfaces

Post by imamelia »

I can provide a video.

https://www.youtube.com/watch?v=JLmovRwtBYU

It's not working correctly with inside corners; it doesn't transition to the ceiling correctly, only staying there for 1 frame and then falling.
jeffythedragonslayer
Posts: 241
Joined: Thu Dec 09, 2021 12:29 pm

Re: Making an enemy move along surfaces

Post by jeffythedragonslayer »

Could you try setting breakpoints and single stepping through it in Mesen-S or bsnes-plus?
imamelia
Posts: 14
Joined: Wed Sep 01, 2010 12:17 am

Re: Making an enemy move along surfaces

Post by imamelia »

I've been doing that. I think it detects the ceiling one pixel early or something.
none
Posts: 111
Joined: Thu Sep 03, 2020 1:09 am

Re: Making an enemy move along surfaces

Post by none »

The ghost on the left hand side in the video seems to be able to travel along inside corners just fine though, without falling of.

Maybe it's a problem with specific patterns of your 1,2,3,4 blocks?

Can you add more different test cases, like in this screenshot (I hope it's understandable)?
Screenshot_2022-09-22_22-34-11.png
imamelia
Posts: 14
Joined: Wed Sep 01, 2010 12:17 am

Re: Making an enemy move along surfaces

Post by imamelia »

Okay, both of those also caused glitches. The addition of the block on the right resulted in the same problem as before.
none
Posts: 111
Joined: Thu Sep 03, 2020 1:09 am

Re: Making an enemy move along surfaces

Post by none »

imamelia wrote: Thu Sep 22, 2022 2:41 pm Okay, both of those also caused glitches. The addition of the block on the right resulted in the same problem as before.
it falls of as soon as it touches the new block, or does it manage to climb that block and then falls of as soon as it reaches the ceiling?
imamelia
Posts: 14
Joined: Wed Sep 01, 2010 12:17 am

Re: Making an enemy move along surfaces

Post by imamelia »

It falls off once it touches the new block.

Also, the first formation doesn't work when it goes counterclockwise.

Edit: I think some of the offsets might actually have been wrong. I changed them to what should be the correct values, and now the first formation works for both directions, but the second and third don't work for either. This is the new offset table:

Code: Select all

BlockCheckData:
; 00: ground
	dw $0010,$0010 : dw $0010,$0000 : dw $0000,$0010
	dw $FFF0,$0010 : dw $FFF0,$0000 : dw $0000,$0010
; 01: ceiling
	dw $FFF0,$FFF0 : dw $FFF0,$0000 : dw $0000,$FFF0
	dw $0010,$FFF0 : dw $0010,$0000 : dw $0000,$FFF0
; 02: outer left edge
	dw $0010,$FFF0 : dw $0000,$FFF0 : dw $0010,$0000
	dw $0010,$0010 : dw $0000,$0010 : dw $0010,$0000
; 03: outer right edge
	dw $FFF0,$0010 : dw $0000,$0010 : dw $FFF0,$0000
	dw $FFF0,$FFF0 : dw $0000,$FFF0 : dw $FFF0,$0000
I also tried making the block check only happen when both the X and Y position are evenly divisible by 0x10. Whether that will screw up once I get to the slopes, I'm not sure.
Post Reply