Unlimited Warps -- One Screen

Bucket Mouse

Active member
Earlier this month I posted eagerly about the invention of the Multi-Tile. There was no enthusiasm or any sort of reaction whatsoever from anyone else. I still don't understand why. If you can make a tile do more than one thing, then the small number of tiles you're given is no longer a limitation. I can do all sorts of things with just one tile now, like for example, warping to fifteen different screens from just one screen. Using just one tile.

The old warp system works by directly assigning warp information to each screen. Every warp tile you place on that screen will go to that location and that location only. You can duplicate the code and make another tile that takes you somewhere else, but if you keep doing that, you'll run out of tiles quickly.

If only you could directly TELL the tile, "this door leads to THIS house and THIS door leads to THAT house." What we need is a tile smart enough to know where exactly on the screen you are, and based on that information, where to send you. Voila!

****

Before we begin, a word of caution: this is not a plug-and-play type of code. You will need to customize it for the needs of the game you're applying it toward. If you're just starting out with NESMaker or ASM you probably won't know what to do. What specifically do you need to know? For starters, how to read the grid of the screen.

Each screen is a 16 by 16 grid, made up of 16 by 16 pixels. They work on a Base 16 numerical system -- hexadecimal numbers, 0 through 9 and then A through F. Each of those pixels has an assigned hex code. In order for the multi-warp tile to recognize where it should do something, you first have to tell it "if the player is standing on the tile and their coordinates are within THIS hex range, then warp to THIS screen." You must know the addresses of your warps.

Fortunately using any Overworld or Underworld screen this is rather simple. If you drag your mouse over the map screen you'll notice little numbers above that tell you the X and Y coordinates of each graphics square. So say you want the warp to take you to screen #1 if the tile is placed on X=2 and Y=4.

Easy. X in this case is #$20 and Y would be #$40. You just have to add a zero (and also convert the number to a letter if it's 10 or over). Here if the player and the tile intersect in the range of #$20 to #$2F horizontally and #$40 to #$4F vertically, a warp should be activated to screen #$01.

HOWEVER if you simply enter that into the code, the sprite will need to be spot-on to activate the warp. Nine times out of ten the sprite will simply hit half the square and nothing will happen, so to allow for this, you must overshoot the numbers by 1 in each direction.

Now that we've made that modification, we have #$10 to #$30 for X and #$30 to #$50 for Y. All we need is the hex code for the screen we want.

The screens are, again, arranged in a 16 by 16 grid for both the Overworld and the Underworld so we get those numbers the same way. Hold your mouse over where you want the player to travel.

Here's where things are a bit different because screens are not named with X and Y coordinates. Instead they're given specific hex numbers. So if your screen is X=11 and Y=0 the actual name of the screen is #$0B.

Now we have the information to make a multi-warp tile.

*****

Below is the rough version of my multi-warp program. You'll notice a lot of repetition in the code. You'll also notice at the beginning I establish variables that I only ended up using once -- this is because I was setting up a loop to cut down on the repeated commands, and I couldn't get the loop to work. Something in the math was wrong. Maybe it's better to post this version since you can read the flow of what's happening more easily.

The program is written to work with the screen warp layout seen in this Facebook post: https://www.facebook.com/groups/NESmakers/permalink/874227152931611/
There are fifteen warps on the screen, five each in three rows. First the program determines which row you're standing on and then which square in that row.

Code:
; MACRO GoToScreen arg0, arg1, arg2
	; arg 0 = screen to warp to.
	; arg 1 = map, usually #$01
	; arg 2 = transition type, usually #$02
	
;	A graphics block is made up of 16 pixels...hex numbers are base 16
;	Look at the overworld grid to determine the coordinates
;	If the door is at X=2 and Y=4, the coordinates for X are #$20 through #$2F and Y is #$40 through #$4F
;	Overshoot your numbers by 1 to compensate for half-touches

;	Try to put your doors equal increments apart, so you can create a loop

	LDA #$10
	STA warpXV
	LDA #$30
	STA warpYV
	
	LDA #$00
	STA warpXH
	LDA #$20
	STA warpYH
	
	LDA #$01
	STA screen2warp2

	LDX player1_object
	LDA Object_y_hi,x ; load vertical location of player
	CMP warpXV
	BCC NotSpot1 ; branch if it's less than the spot's general area
	CMP warpYV
	BCS NotSpot1 ; branch if it's more than the spot's general area
	JMP RowSelected ; looks like the player lines up vertically...jump to the horizontal part
NotSpot1: ; the player is not on Row 1, so let's check Row 2
	CMP #$50
	BCC NotSpot2 ; branch if it's less than the spot's general area
	CMP #$70
	BCS NotSpot2 ; branch if it's more than the spot's general area
	LDA screen2warp2
	CLC
	ADC #$05 ; add five to screenwarp number since we skipped the five squares in Row 1
	STA screen2warp2
	JMP RowSelected ; looks like the player lines up vertically...jump to the horizontal part
NotSpot2: ; the player is not on Row 2, so let's check Row 3
	CMP #$A0
	BCC NotSpot3 ; branch if it's less than the spot's general area
	CMP #$C0
	BCS NotSpot3 ; branch if it's more than the spot's general area
	LDA screen2warp2
	CLC
	ADC #$0A ; add ten to screenwarp number
	STA screen2warp2
	JMP RowSelected ; looks like the player lines up vertically...jump to the horizontal part
	NotSpot3:
JMP Notanything ; the player is not on any of the tiles, so the program is over


RowSelected:
	LDA Object_x_hi,x ; load horizontal location of player
	CMP warpXH
	BCC Notdoor1
	CMP warpYH
	BCS Notdoor1
	GoToScreen screen2warp2, #$01, #$02 ; We're in the X and Y zone -- we can warp
	JMP Notanything

Notdoor1:
	INC screen2warp2
	CMP #$30
	BCC Notdoor2
	CMP #$50
	BCS Notdoor2
	GoToScreen screen2warp2, #$01, #$02 ; We're in the X and Y zone -- we can warp
	JMP Notanything
	
	Notdoor2:
	INC screen2warp2
	CMP #$60
	BCC Notdoor3
	CMP #$80
	BCS Notdoor3
	GoToScreen screen2warp2, #$01, #$02 ; We're in the X and Y zone -- we can warp
	JMP Notanything
	
	Notdoor3:
	INC screen2warp2
	CMP #$90
	BCC Notdoor4
	CMP #$B0
	BCS Notdoor4
	GoToScreen screen2warp2, #$01, #$02 ; We're in the X and Y zone -- we can warp
	JMP Notanything
	
	Notdoor4:
	INC screen2warp2
	CMP #$C0
	BCC Notdoor5
	CMP #$E0
	BCS Notdoor5
	GoToScreen screen2warp2, #$01, #$02 ; We're in the X and Y zone -- we can warp
	JMP Notdoor5

Notdoor5:


Notanything:

The bugs discussed below have now been fixed. Let me know if you find any more.
 

Kasumi

New member
RE: The bug. (Maybe it's not THE bug, but it's definitely a potential bug.) Inside the go to screen macro is this code:

Code:
MACRO GoToScreen arg0, arg1, arg2
                                	; arg 0 = screen to warp to.
                                	; arg 1 = map
                                	; arg 2 = transition type
                                
                                	LDA arg0
                                	STA temp2
                                	LDA arg1
                                	STA temp3
                                	LDA arg2
                                	STA temp1
                                	JSR HandleGoToScreen
HandleGoToScreen looks like this:

Code:
0FB44                           HandleGoToScreen:
0FB44                           	.include "Routines\Basic\ModuleScripts\HurtWinLoseDeath\WarpToScreen.asm"
0FB44                           	;; used with macro
0FB44                           		
0FB44 A5 3C                     		LDA temp2
0FB46 9D A2 05                  			STA Object_scroll,x
0FB49 8D 5E 04                  			STA currentNametable
0FB4C 8D 97 06                  			STA newScreen
0FB4F 8D 98 06                  			STA currentScreen ;currentScreen
0FB52 8D 4F 04                  			STA xScroll_hi
0FB55 8D 6F 04                  			STA nt_hold
0FB58 BD A2 05                  			LDA Object_scroll,x
0FB5B 29 01                     			AND #%00000001
0FB5D 8D 52 04                  			STA showingNametable
0FB60 A5 B3                     			LDA newX
0FB62 9D 9E 04                  			STA Object_x_hi,x
0FB65 85 5A                     			STA xHold_hi
Note that it modifies Object_x_hi.

So with code like this:
Code:
	LDA Object_x_hi,x ; load horizontal location of player
	CMP #$30
	BCC Notdoor2
	CMP #$50
	BCS Notdoor2
	GoToScreen screen2warp2, #$01, #$00 ; We're in the X and Y zone -- we can warp
	
	Notdoor2:
	INC screen2warp2
	LDA Object_x_hi,x ; load horizontal location of player
You load Object_x_hi, then see if it's in a range. If it's within that range, you run the macro (which does all the code I posted above, plus a little more). So far so good. But it doesn't end the routine. It just goes to Notdoor2. Which adds one to screen2wrap2. And loads Object_x_hi again, except GoToScreen above may have changed it.

So it's very possible for two GoToScreens to run with only one iteration of this loop. To fix, you want a jmp Notanything after every GoToScreen macro.
 

Bucket Mouse

Active member
Kasumi said:
So it's very possible for two GoToScreens to run with only one iteration of this loop. To fix, you want a jmp Notanything after every GoToScreen macro.
Well....no I don't, because if screen2warp2 isn't being updated, it won't go to the right screen. However, since nothing else gets loaded into the Accumulator throughout the row scan process, I really only need one Object_x_hi,x......
Getting rid of the duplicates there seems to fix the bug. Thanks. Code updated.

The warp position problem is still there though. Hopefully someone knows how to fix that one.
 

Kasumi

New member
screen2warp2 is provided to GoToScreen, so it would still go to the right screen. If it gets updated after a GoToScreen, but is never passed to a GoToScreen, it getting updated wouldn't have an effect.

The updated code you posted has a similar problem.
Code:
LDA Object_x_hi,x ; load horizontal location of player
	CMP warpXH
	BCC Notdoor1
	CMP warpYH
	BCS Notdoor1
	GoToScreen screen2warp2, #$01, #$00 ; We're in the X and Y zone -- we can warp

Notdoor1:
	INC screen2warp2
	CMP #$30
	BCC Notdoor2
GoToScreen changes A, so Object_x_hi,x is not in A at the Notdoor1 label if that GoToScreen macro is run. My suggestion was to make the routine jmp to the end instead of continuing down to the next check after each macro to avoid issues like that. I realize it appears to work, but say GoToScreen makes A $30 in some cases. The condition for the next door will be satisfied too.

I promise I'm not trying to hassle you, it's just that even if this works for now it's luck. You're betting on what GoToScreen happens to change A to not satisfying the next range check condition, when you can just not run the next range check instead.
 

Kasumi

New member
screen2warp2 is provided to GoToScreen, so it would still go to the right screen. If it gets updated after a GoToScreen, but is never passed to a GoToScreen, it getting updated wouldn't have an effect.

The updated code you posted has a similar problem.
Code:
LDA Object_x_hi,x ; load horizontal location of player
	CMP warpXH
	BCC Notdoor1
	CMP warpYH
	BCS Notdoor1
	GoToScreen screen2warp2, #$01, #$00 ; We're in the X and Y zone -- we can warp

Notdoor1:
	INC screen2warp2
	CMP #$30
	BCC Notdoor2
GoToScreen changes A, so Object_x_hi,x is not in A at the Notdoor1 label if that GoToScreen macro is run. My suggestion was to make the routine jmp to the end instead of continuing down to the next check after each macro to avoid issues like that. I realize it appears to work, but say GoToScreen makes A $30 in some cases. The condition for the next door will be satisfied too.

I promise I'm not trying to hassle you, it's just that even if this works for now it's luck. You're betting on what GoToScreen returns not being in the next range check, when you can just not run the next range check.
 

Bucket Mouse

Active member
Kasumi said:
screen2warp2 is provided to GoToScreen, so it would still go to the right screen. If it gets updated after a GoToScreen, but is never passed to a GoToScreen, it getting updated wouldn't have an effect.

The updated code you posted has a similar problem.
Code:
LDA Object_x_hi,x ; load horizontal location of player
	CMP warpXH
	BCC Notdoor1
	CMP warpYH
	BCS Notdoor1
	GoToScreen screen2warp2, #$01, #$00 ; We're in the X and Y zone -- we can warp

Notdoor1:
	INC screen2warp2
	CMP #$30
	BCC Notdoor2
GoToScreen changes A, so Object_x_hi,x is not in A at the Notdoor1 label if that GoToScreen macro is run. My suggestion was to make the routine jmp to the end instead of continuing down to the next check after each macro to avoid issues like that. I realize it appears to work, but say GoToScreen makes A $30 in some cases. The condition for the next door will be satisfied too.

I promise I'm not trying to hassle you, it's just that even if this works for now it's luck. You're betting on what GoToScreen happens to change A to not satisfying the next range check condition, when you can just not run the next range check instead.

screen2warp2 is the literal value of the screen being traveled to. Nothing else is increasing its value -- GoToScreen does not do that on its own. If I took the INC out, screen2warp2 would take every single one of those warps to Screen 1.

And it would pass to a GoToScreen IF the previous GoToScreen was never read, such as in the case of the player standing on the second tile instead of the first. There's no scenario here where screen2warp2 is updated for no reason.
 

Kasumi

New member
I'll try to explain it another way:
Under Notdoor1, screen2warp2 is incremented.

However, there are three ways to get to Notdoor1, and I think you think there are only 2.

The first way: When Object_x_hi,x is less than warpXH, in which case it branches there. screen2warp2 is incremented, because the player wasn't inside the first door. (was too far left)
The second way: When Object_x_hi,x was greater than or equal to warpXH, but was greater than or equal to warpYH, in which case, it branches there. screen2warp2 is incremented, because the player wasn't inside the first door. (was too far right)

The third (problematic) way: When Object_x_hi,x was greater than or equal to warpXH, and was less than warpYH. In this case, GoToScreen is run. Then, the code runs down to Notdoor1. screen2warp2 is STILL incremented, despite the fact player was in the first door, and GoToScreen was already run with the correct value.

You have two branches to Notdoor1, but the code will also naturally travel to Notdoor1 when the player walks over door 1. I am not suggesting you take the inc out. I am suggesting you make it impossible to get to Notdoor1 in case 3.
 

Bucket Mouse

Active member
Okay, I think I see what you're talking about. But I was under the impression that once I told the program to Gotoscreen, that was it. Whatever else was in that code would be ignored since the command was to load a whole new screen over it. I mean, that makes sense, doesn't it? It's the last thing it would read.
 

Kasumi

New member
All a macro does is copy the code within the macro to the place where you put the macro (but with small replacements for the arguements [args]). So this is the macro:
Code:
MACRO GoToScreen arg0, arg1, arg2
                                	; arg 0 = screen to warp to.
                                	; arg 1 = map
                                	; arg 2 = transition type
                                
                                	LDA arg0
                                	STA temp2
                                	LDA arg1
                                	STA temp3
                                	LDA arg2
                                	STA temp1
                                	JSR HandleGoToScreen
                                	
                                
                                	
                                	
                                	ENDM

And this is three lines of your code:
Code:
BCS Notdoor1
	GoToScreen screen2warp2, #$01, #$00 ; We're in the X and Y zone -- we can warp

Notdoor1:

And this is what the above code ends up as:

Code:
BCS Notdoor1
LDA screen2warp2
                                	STA temp2
                                	LDA #$01
                                	STA temp3
                                	LDA #$00
                                	STA temp1
                                	JSR HandleGoToScreen

Notdoor1:

HandleGoToScreen is a subroutine, so when it hits its RTS, the code will continue to Notdoor1.

It is a reasonable assumption that GoToScreen might exit the current code, but it does not. It sets up variables so loading the new screen will happen later. The reason it does it in this way is likely because you could put that macro inside a subroutine, and exiting by jmp within a subroutine would slowly corrupt the stack.
 

Bucket Mouse

Active member
Okay. But now I'm trying to figure out how to get it to read the warp coordinates of the loading screen. These are the contents of Warptoscreen.asm:

Code:
LDA #$00
STA newGameState
 LDA warpMap
sta currentMap
 clc
 ADC #$01
 STA temp
 GoToScreen #$00, temp, #$03
 LDA #$00
 STA playerToSpawn
 LDX player1_object
 DeactivateCurrentObject
 LDA #$01
 STA loadObjectFlag
 
LDA mapPosX
STA newX
LDA mapPosY
STA newY

"newX" and "newY" sound like the missing coordinates I need. So I fit this code into my own:

Code:
RowSelected:
	LDA Object_x_hi,x ; load horizontal location of player
	CMP warpXH
	BCC Notdoor1
	CMP warpYH
	BCS Notdoor1
LDA #$00
STA newGameState
 LDA warpMap
STA currentMap
	GoToScreen screen2warp2, #$01, #$00 ; We're in the X and Y zone -- we can warp
	JSR MapPosition
	JMP Notanything

Code:
	MapPosition:
	 LDA #$00
 STA playerToSpawn
 LDX player1_object
 DeactivateCurrentObject
 LDA #$01
 STA loadObjectFlag
 
LDA mapPosX
STA newX
LDA mapPosY
STA newY
RTS
Nothing. No change. Player still spawns on the edge. All the number screens are set for the player to warp in the middle.
Why can't this happen?
 

Kasumi

New member
GoToScreen takes three arguments. The screen to warp to, the map, and the transition type. The transition type is stored to temp1 to before going to HandleGoToScreen. Let's see what HandleGoToScreen uses temp1 for.
Code:
LDA temp1
	STA screen_transition_type
Well, it just stores it directly to screen_transition_type. So... let's see how that is used elsewhere in the code. Ah:
Code:
LDA screen_transition_type
	BNE notStartingGameTransitionType

	;; set newX and newY to the game start positions
	LDA #START_POSITION_PIX_X 
	STA newX
	LDA #START_POSITION_PIX_Y
	STA newY
	JMP doneWithScreenTransition
notStartingGameTransitionType:
	CMP #$01
	BNE notNormalScreenToScreenUpdate

So if the transition type is 0, newX and newY get set to the starting position. (This is the type your macro is currently using), even if you did write something to newX and newY, it would just get overwritten. But this doesn't necessarily confirm what newX and newY actually do (where they get stored) in all cases. And this is the part where I get slightly confused and can't follow the logic. NES Maker seems to set newX and newY after it is used, and in strange places depending on the transition type. But it seems like simply by changing the transition type to... uh... 2? Maybe 3. It might work if you set newX and newY that way.

And if not uh... then maybe dale is needed after all, I have limits to how deep I'm willing to go. I have tried to lay out a process here for how one might find similar info, though. (Admittedly a little brief. But really, searching demo.txt is magical!)
 

Bucket Mouse

Active member
Okay. So it turns out whatever you put in newX and newY are the coordinates the new screen will put you at, no matter what. #START_POSITION_PIX_X and #START_POSITION_PIX_Y are what they sound like -- the starting position for the entire game, so wherever you played the player icon at the starting screen is the place they'll appear on every screen. Putting #$70 in both slots deposits you in the middle of the screen. I can only guess mapposX and mapposY are loaded with nothing,

And why wouldn't they be? I can't find a single instance in this entire engine where mapposX and mapposY are given values. I can only find places where they're used. How do they work in the first place?

At what point do newX and newY get loaded with the set coordinates for the next screen, as set in NESMaker? It can't be the point in Warptoscreen.asm.

I could fake it by creating two new variables that load the coordinates I want for each screen. But I shouldn't have to.

UPDATE: I looked at an earlier experiment I had done with multi-warps (where I simply made new tiles with hardcoded warp coordinates) to see if anything was different. I noticed that the third number (the transition type) was #$02, not $00.
"It can't be THAT simple, can it?" I said. ..........................................It can. There's one Saturday wasted.
 

dale_coop

Moderator
Staff member
If you want your newX and newY to be used... you have to use the 02 warp type:
Code:
. GoToScreen #$00, temp, #$02
This last parameter is the transition type... and if I remember well only the transition type 2 reads mapX and mapY...
For infos: the transition type reads the mapPosX and mapPosY... the 04 the continuePositionX and continuePositionY (used for respawn after a death)... the 00 reads the #START_POSITION_PIX_X and #START_POSITION_PIX_Y (for the coords of the starting screen) and the 01 is normal screen transition.
 

Bucket Mouse

Active member
dale_coop said:
If you want your newX and newY to be used... you have to use the 02 warp type:
Code:
. GoToScreen #$00, temp, #$02
This last parameter is the transition type... and if I remember well only the transition type 2 reads mapX and mapY...
For infos: the transition type reads the mapPosX and mapPosY... the 04 the continuePositionX and continuePositionY (used for respawn after a death)... the 00 reads the #START_POSITION_PIX_X and #START_POSITION_PIX_Y (for the coords of the starting screen) and the 01 is normal screen transition.

I figured that out right before you posted it. >_< I thought "transition type" had to do with fade-ins and was invalid.
 

mouse spirit

Well-known member
This looks awesome bucket but i have a simple problem before i can even try. I cant get a warp to work to the underworld. I can start in the underworld, but not warp to it. I am using the 4.1.5 platformer and i just mean the basic function i cant get to work.

Tried checking the warp out to underworld box on both screens together and seperately and without it checked.

I have tried to read stuff on this but maybe i missed a fix post.
 
Top Bottom