Making an enemy move along surfaces

Discussion of hardware and software development for Super NES and Super Famicom. See the SNESdev wiki for more information.

Moderator: Moderators

Forum rules
  • For making cartridges of your Super NES games, see Reproduction.
Post Reply
imamelia
Posts: 16
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: 1565
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: 117
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: 16
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.
User avatar
jeffythedragonslayer
Posts: 344
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: 16
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: 117
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: 16
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: 117
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: 16
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.
imamelia
Posts: 16
Joined: Wed Sep 01, 2010 12:17 am

Re: Making an enemy move along surfaces

Post by imamelia »

All right, here's the code currently.

Code: Select all

; Defines, RAM addresses:
; - !E4,x: X position low byte
; - !14E0,x: X position high byte
; - !D8,x: Y position low byte
; - !14D4,x: Y position high byte
; - !C2,x: Current surface state.  See the state list below.
; - !1504,x: Current speed (unsigned).  Always #$10 (1 pixel per frame) for now.
; - !1510,x: Movement direction.  0 = clockwise, 1 = counterclockwise.
; - $018022: Subroutine for moving along the X axis only.
; - $01801A: Subroutine for moving along the Y axis only.
; - $0050xx: Breakpoints.

; States:
;	00 - ground
;	01 - ceiling
;	02 - outer left wall/inner right wall
;	03 - outer right wall/inner left wall
;	2x - on slope (x = 0 or 1)
;	4x - transitioning around an outside corner (x = 0, 1, 2, 3)
;	6x - transitioning to an upward ground slope or downward ceiling slope (x = 0 or 1)
;	FF - air

; X and Y offset of the block to check when turning around an outside corner, to determine when to transition back to normal state
TransitionBlockCheckData:
; 00: ground
	dw $0000,$0010 : dw $000F,$0010
; 01: ceiling
	dw $000F,$FFFF : dw $0000,$FFFF
; 02: outer left edge
	dw $0010,$000F : dw $0010,$0000
; 03: outer right edge
	dw $FFFF,$0000 : dw $FFFF,$000F
; slope transitions don't have special handling


;------------------------------------------------
; the actual routine to handle moving along surfaces
;------------------------------------------------

; if in the air:
;	1) update position with gravity
;	2) check for surface contact
;	3) if there is contact, immediately switch to the corresponding state; if not, do nothing else
; if on the ground and not on a slope:
;	1) check block that is about to be moved into (current X position +$10/-$01; current Y position)
;	2) if this block is a wall, immediately switch to that state
;	3) if this block is a slope, switch to the "transitioning to upward ground slope" state
;	4) if this block is air, continue
;	5) if the block immediately below the current one (current X position +$00/+$0F; current Y position +$10) is solid, don't do anything else
;	6) if the block immediately below the current one is air, switch to the "transitioning around outside corner" state and clear the slope flag
;	7) check if the sprite is above a slope (current X position +$08/+$08; current Y position +$10)
;	8) if that is true, set the "on slope" flag
; if on the ground and on a slope:
;	1) check the current tile number according to the bottom midpoint (X+$08,Y+$10)
;	2) if this tile is not a slope, switch back to the normal ground state
;	3) if this tile is a slope, continue
;	4) look up the slope offset table at $00E632 according to the sprite's current X position
;	5) set the sprite's Y position such that the bottom midpoint is exactly on top of the slope
; if on the ceiling and not on a slope:
;	1) check block that is about to be moved into (current X position +$10/-$01; current Y position)
;	2) if this block is a wall, immediately switch to that state
;	3) if this block is a slope, switch to the "transitioning to downward ceiling slope" state
;	4) if this block is air, continue
;	5) if the block immediately above the current one (current X position +$00/+$0F; current Y position -$01) is solid, don't do anything else
;	6) if the block immediately above the current one is air, switch to the "transitioning around outside corner" state and clear the slope flag
;	7) check if the sprite is below an upside-down slope (current X position +$08/+$08; current Y position -$01)
;	8) if that is true, set the "on slope" flag
; if on the ceiling and on a slope:
;	1) check the current tile number according to the top midpoint (X+$08,Y+$00)
;	2) if this tile is not a slope, switch back to the normal ceiling state
;	3) if this tile is a slope, continue
;	4) look up the slope offset table at $00E632 according to the sprite's current X position
;	5) set the sprite's Y position such that the top midpoint is exactly at the bottom of the slope
; if on the wall:
;	1) check block that is about to be moved into (current X position; current Y position +$10/-$01)
;	2) if this block is a floor/ceiling, immediately switch to that state
;	3) if this block is a slope, switch to either the ground or ceiling state and set the "on slope" flag
;	4) if this block is air, continue
;	5) if the block immediately to the left or right of the current one (current X position +$10 or -$01; current Y position +$00 or +$0F) is solid, don't do anything else
;	6) if the block immediately to the left or right of the current one is air, switch to the "transitioning around outside corner" state and clear the slope flag
; if in a transition state:
;	1) if transitioning to slope, check if the top or bottom midpoint (current X position +$08; current Y position +$10/-$01) is on a slope tile
;	2) if it is, set the "on slope" state
;	3) if it isn't, end and immediately proceed to the normal speed routine
;	4) if transitioning around a corner, check if the tile at the beginning of the new surface (X+$00/+$0F and Y+$10/-$01 or X+$10/-$01 and Y+$00/+$0F) is solid
;	5) if it is, clear the transition state flag

WallFollowHandler:
; concatenate the X and Y positions for 16-bit referencing
STA $005000
	LDY !1510,x
	LDA !C2,x
	BPL .NotInAir
; state FF: in air
	JSL $01802A|!bank
STA $005002
	LDY #$00
	LDA !1588,x
	BIT #$84
	BNE .HitSurface
	INY
	BIT #$28
	BNE .HitSurface
	INY
	BIT #$41
	BNE .HitSurface
	INY
	BIT #$42
	BEQ .Return
.HitSurface
STA $005004
; if it hit a surface, immediately switch to that state
	TYA
	LDY !15B8,x
	BEQ $02
	ORA #$20
	STA !C2,x
.Return
	RTS
.NotInAir
	BEQ .OnFlatGround
	JMP .NotOnFlatGround
.OnFlatGround
; state 00: on the ground and not on a slope or in a transition
STA $005010
	JSR MoveHoriz
STA $005012
	REP #$20
	LDA #$0010
	CPY #$00
	BEQ ..OffsetDir0
	LDA #$FFFF
..OffsetDir0
; determine the offset of the block that we're moving into, then add it to the base position
	CLC
	ADC $8A
	STA $00
	LDA $8C
	STA $02
	JSR CheckBlockType
STA $005014
	BEQ ..Continue
	BVS ..MoveIntoSlope
	BCC .Return
..SwitchToWall
; if the tile we're moving into is solid, switch to the "on wall" state (which wall depends on direction)
	LDA !1510,x
	ORA #$02
	STA !C2,x
	RTS
..MoveIntoSlope
; if the tile we're moving into is a slope, switch to the "transition to slope" state
	LDA #$60
	STA !C2,x
..Return
	RTS
..Continue
; if the tile we're moving into is air, do more checks
STA $005016
	REP #$20
	LDA $8A
	LDY !1510,x
	BEQ ..NoAdd
	CLC
	ADC #$000F
..NoAdd
	STA $00
	LDA $8C
	CLC
	ADC #$0010
	STA $02
	JSR CheckBlockType
STA $005018
; if the block immediately below the current one is solid, don't do anything else
	BCS ..Return
	BNE ..MaybeAboveSlope
..AboveAir
; if the block immediately below the current one is air, switch to the "transitioning around outside corner" state and clear the slope flag
	LDA !1510,x
	EOR #$03
	ORA #$40
	STA !C2,x
	RTS
..MaybeAboveSlope
STA $00501A
	REP #$20
	LDA $8A
	CLC
	ADC #$0008
	STA $00
	LDA $8C
	CLC
	ADC #$0010
	STA $02
	JSR CheckBlockType
	BVC ..Return
; if the sprite is above a slope, set the "on slope" flag and move it down a pixel
	LDA !C2,x
	ORA #$20
	STA !C2,x
	INC !D8,x
	BNE $03
	INC !14D4,x
	RTS
.NotOnFlatGround
	BIT !C2,x
	BVC .NotTransition
	JMP .TransitionState
.NotTransition
	BIT #$20
	BNE .OnSlope
	JMP .NotOnSlope
.OnSlope
STA $005020
; states 20 and 21: on a ground or ceiling slope
	JSR MoveHoriz
STA $005022
	REP #$20
	LDA $8A
	CLC
	ADC #$0008
	STA $00
	LDA !C2,x
	LSR
	LDA $8C
	BCS .NoOffset
	CLC
	ADC #$0010
.NoOffset
	STA $02
	JSR CheckBlockType
STA $005024
	BVS ..Continue
	LDA !C2,x
	AND #$DF
	STA !C2,x
	RTS
..Continue
STA $005026
	JMP GetSlopeOffset
.NotOnSlope
	BIT #$02
	BEQ .NotOnWall
	JMP .OnWall
.NotOnWall
; state 01: on the ceiling and not on a slope or in a transition
STA $005030
	JSR MoveHoriz
STA $005032
	REP #$20
	LDA #$0010
	CPY #$00
	BEQ ..OffsetDir0
	LDA #$FFFF
..OffsetDir0
; determine the offset of the block that we're moving into, then add it to the base position
	CLC
	ADC $8A
	STA $00
	LDA $8C
	STA $02
	JSR CheckBlockType
STA $005034
	BEQ ..Continue
	BVS ..MoveIntoSlope
	BCC ..Return
..SwitchToWall
; if the tile we're moving into is solid, switch to the "on wall" state (which wall depends on direction)
	LDA !1510,x
	EOR #$03
	STA !C2,x
	RTS
..MoveIntoSlope
; if the tile we're moving into is a slope, switch to the "transition to slope" state
	LDA !C2,x
	AND #$01
	ORA #$60
	STA !C2,x
..Return
	RTS
..Continue
; if the tile we're moving into is air, do more checks
STA $005036
	REP #$20
	LDA $8A
	LDY !1510,x
	BNE ..NoAdd
	CLC
	ADC #$000F
..NoAdd
	STA $00
	LDA $8C
	SEC
	SBC #$0010
	STA $02
	JSR CheckBlockType
STA $005038
; if the block immediately above the current one is solid, don't do anything else
	BCS ..Return
	BNE ..MaybeBelowSlope
..BelowAir
; if the block immediately above the current one is air, switch to the "transitioning around outside corner" state and clear the slope flag
	LDA !1510,x
	EOR #$02
	ORA #$40
	STA !C2,x
	RTS
..MaybeBelowSlope
STA $00503A
	REP #$20
	LDA $8A
	CLC
	ADC #$0008
	STA $00
	LDA $8C
	SEC
	SBC #$0010
	STA $02
	JSR CheckBlockType
	BVC ..Return
STA $00503C
; if the sprite is below a slope, set the "on slope" flag and move it a pixel toward the slope
	LDA !C2,x
	ORA #$20
	STA !C2,x
	LDA !D8,x
	BNE $03
	DEC !14D4,x
	DEC !D8,x
	RTS
.OnWall
STA $005040
; states 02 and 03: on a wall
	JSR MoveVert
STA $005042
	REP #$20
	LDA $8A
	STA $00
	LDA $8C
; offset is #$FFFF if we're on the left wall going clockwise or right wall going counterclockwise (going up) and #$0010 otherwise (going down)
	DEC
	LDY $8E
	BEQ ..UpOffset
	CLC
	ADC #$0011
..UpOffset
	STA $02
	JSR CheckBlockType
STA $005044
	BEQ ..Continue
	BVS ..MoveIntoSlope
	BCC ..Return
..SwitchToHoriz
; if the tile we're moving into is solid, switch to the ground or ceiling state (which one depends on the wall we're on and the current direction)
	LDA $8E
	EOR #$01
	STA !C2,x
	RTS
..MoveIntoSlope
; if the tile we're moving into is a slope, switch to the "transition to slope" state and also the ground or ceiling
	LDA $8E
	EOR #$01
	ORA #$60
	STA !C2,x
..Return
	RTS
..Continue
STA $005046
	LDA !C2,x
	LSR
	REP #$20
	LDA $8A
; X offset is #$FFFF if we're on the right wall and #$0010 if we're on the left wall
	DEC
	BCS ..RightOffset
	CLC
	ADC #$0011
..RightOffset
	STA $00
	LDA $8C
	LDY $8E
; Y offset is #$0000 if we're going down and #$000F if we're going up
	BNE ..NoYOffset
	CLC
	ADC #$000F
..NoYOffset
	STA $02
	JSR CheckBlockType
STA $005048
	BNE ..Return
; if the block immediately to the left or right of the current one is air, switch to the "transitioning around outside corner" state and clear the slope flag
	LDA $8E
	ORA #$40
	STA !C2,x
	RTS
.TransitionState
	AND #$20
	BEQ .TransitionAroundCorner
; states 6x: transitioning to a slope
STA $005050
	JSR MoveHoriz
STA $005052
	REP #$20
; check if the bottom/top midpoint is on a slope tile
	LDA $8A
	CLC
	ADC #$0008
	STA $00
	LDA $8C
	DEC
	LDY $8E
	BEQ ..UpOffset
	CLC
	ADC #$0011
..UpOffset
	STA $02
	JSR CheckBlockType
STA $005054
	BVC ..Return
	LDA !C2,x
	AND #$BF
	STA !C2,x
..Return
	RTS
.TransitionAroundCorner
; states 4x: transitioning around an outside corner
STA $005060
	LDA !C2,x
	AND #$02
	BNE ..Vertical
..Horizontal
	JSR MoveHoriz
	BRA ..Shared
..Vertical
	JSR MoveVert
..Shared
STA $005062
; the X and Y offsets of the block to check depend on the surface and the direction
	LDA !C2,x
	AND #$03
	ASL #3
	STA $00
	LDA !1510,x
	ASL #2
	ORA $00
	TAY
	REP #$20
	LDA $8A
	CLC
	ADC TransitionBlockCheckData,y
	STA $00
	LDA $8C
	CLC
	ADC TransitionBlockCheckData+2,y
	STA $02
	JSR CheckBlockType
STA $005064
	BCC ..Return
; if the block at the beginning of the new surface is solid, clear the transition flag
	LDA !C2,x
	AND #$BF
	STA !C2,x
..Return
	RTS

;------------------------------------------------
; check what the current Map16 tile number is
;------------------------------------------------

; Input:
; - $00-$01 = X position
; - $02-$03 = Y position
; Return values:
; - Passable tile: Z = 1
; - Solid tile: C = 1, N = 0
; - Ledge tile: C = 1, N = 1
; - Slope tile: V = 1
CheckBlockType:
	SEP #$20
	JSL !FindMap16ActsLike
	REP #$60
	CMP #$00EC
	BCC .ReturnAir
	CMP #$0100
	BCC .ReturnSolid
	CMP #$0111
	BCC .ReturnLedge
	CMP #$016E
	BCC .ReturnSolid
	CMP #$01D8
	BCC .ReturnSlope
; if the tile number was EC-FF, 111-16D, or 1D8-1FF, then it's solid
.ReturnSolid
	REP #$C2
	SEP #$21
	RTS
; if the tile number was 100-110, then it's a passable ledge
.ReturnLedge
	REP #$42
	SEP #$A1
	RTS
; if the tile number was 0-EB, then it's air or non-interactive scenery
.ReturnAir
	REP #$C1
	SEP #$22
	RTS
; if the tile number was 16E-1D7, then it's a slope
.ReturnSlope
	SBC #$016D
	REP #$83
	SEP #$60
	RTS

;------------------------------------------------
; move straight horizontally along the floor/ceiling (or on a slope)
;------------------------------------------------

MoveHoriz:
	LDA !1510,x
	EOR !C2,x
	AND #$01
	STA $8E
	LSR
	LDA !1504,x
	BCC $03
	EOR #$FF : INC
	STA !B6,x
	JSL $018022|!bank
	LDA !E4,x
	STA $8A
	LDA !14E0,x
	STA $8B
	LDA !D8,x
	STA $8C
	LDA !14D4,x
	STA $8D
	RTS

;------------------------------------------------
; move straight vertically along a wall
;------------------------------------------------

MoveVert:
	LDA !1510,x
	EOR !C2,x
	AND #$01
	STA $8E
	LSR
	LDA !1504,x
	BCS $03
	EOR #$FF : INC
	STA !AA,x
	JSL $01801A|!bank
	LDA !E4,x
	STA $8A
	LDA !14E0,x
	STA $8B
	LDA !D8,x
	STA $8C
	LDA !14D4,x
	STA $8D
	RTS

;------------------------------------------------
; find a Y offset depending on the slope type
;------------------------------------------------

; Input: Map16 tile number - #$016E
GetSlopeOffset:
	REP #$30
	TAY
	LDA [$82],y
	AND #$00FF
	ASL #4
	STA $00
	LDA $94
	AND #$000F
	ORA $00
	TAX
	LDA $00E632|!bank,x
	AND #$000F
	CLC
	ADC $02
	SEP #$30
	STA !D8,x
	XBA
	STA !14D4,x
	RTS
It works for surfaces that are only outside corners or only inside ones, but having both throws things off. The culprit seems to be an off-by-one error somewhere that results in what should have been a Y position of n, where n is an even multiple of 0x10, actually ends up being n+1 when going down and right and n-1 when going up and left (furthermore, when going down and left, the X position is 1 less than what it's supposed to be).
imamelia
Posts: 16
Joined: Wed Sep 01, 2010 12:17 am

Re: Making an enemy move along surfaces

Post by imamelia »

Well, I fixed that bug, sort of. It now seems to work correctly with everything except slopes...i.e., the reason why I was doing this in the first place. Five months of annoyance just to find a new way of doing something that I already had code for. And with how finicky all the different offsets and stuff are, I'm not sure if I even could get it to work with slopes. So yeah, this was stupid. Thank goodness I promised myself that I wouldn't work on it after the end of September. One more day and I'll be able to just do a Marie Kondo on this whole cursed project and move on.
Post Reply