Drawing Order in 2D Tile Editor

BlitzPlus Forums/BlitzPlus Programming/Drawing Order in 2D Tile Editor

Adam Bossy(Posted 2004) [#1]
I'm working on a tile editor for my game and ran into the problem of the order the images are drawn.

I want to allow the user to be able to draw to different layers (4 total) and to be able to move tiles forward or backwards in the order they are drawn, as in a graphics program like Photoshop.

Right now, the technique I'm working for is 4 different types, one with a pointer for each layer. The drawing order would be determined by the order that is contained in that type object. Therefore, I would have to use "Before" and "After" to re-sort the tiles everytime the position of one is changed. Has anybody encountered this sort of problem in a tile editor? In what way did you program it in order for it to be efficient and functional?

I'm doing this all in 2D. I know many people suggest using 3D sprites instead. Is the extra work to change from 2D to 3D worth it, or necessary, even? What advantages are there to each?


skn3(Posted 2004) [#2]
Type layers
	Field s.tiles
	Field e.tiles
End Type

Type tiles
	Field image
	Field x
	Field y
	
	Field layer.layers
End Type

Function CreateLayer.layers()
	Local layer.layers
	
	;create new layer
	layer = New layers
	
	Return layer
End Function

Function DeleteLayer(layer.layers)
	Local tile.tiles
	
	;clear tiles from layer
	If layer\s <> Null
		tile = layer\s
		Repeat
			If tile = layer\e
				Delete tile
				Exit
			Else
				tile = After tile
				Delete Before tile
			End If
		Forever
	End If
	
	;delete layer
	Delete layer
End Function

Function CreateTile.tiles(image,x,y,layer.layers)
	Local tile.tiles
	
	;create new tile
	tile       = New tiles
	tile\image = image
	tile\x     = x
	tile\y     = y
	tile\layer = layer
	
	;insert tile into layer
	If layer\s = Null
		;this is first tile in layer, set start pointer to tile
		layer\s = tile
	Else
		;there are other tiles in this layer, add tile to end of list
		Insert tile After layer\e
	End If
	;update the end pointer to this tile
	layer\e = tile
	
	Return tile
End Function

Function DeleteTile(tile.tiles)
	;update layers list of tiles
	If tile = tile\layer\s And tile = tile\layer\e
		;if this tile is the only tile in layer NULL start end pointers (clear the list)
		tile\layer\s = Null
		tile\layer\e = Null
	ElseIf tile = tile\layer\s
		;tile is first in list, update start pointer
		tile\layer\s = After tile
	ElseIf tile = tile\layer\e
		;tile is last in list, update end pointer
		tile\layer\e = Before tile
	End If
	
	;delete tile
	Delete tile
End Function

Function MoveLayerUp(layer.layers)
	;if the layer before this layer exists, then move this layer, before it
	If Before layer <> Null Insert layer Before (Before layer)
End Function

Function MoveLayerDown(layer.layers)
	;if the layer after this layer exists, then move this layer, after it
	If After layer <> Null Insert layer After (After layer)
End Function

Graphics 640,480,32,2

;frame timer
Global timer = CreateTimer(50)

;create 3 layers
Global layer1.layers = CreateLayer()
Global layer2.layers = CreateLayer()
Global layer3.layers = CreateLayer()

;create some tile images
Global image1 = CreateImage(32,32)
Global image2 = CreateImage(32,32)
Global image3 = CreateImage(32,32)
SetBuffer ImageBuffer(image1)
ClsColor 255,0,0
Cls
SetBuffer ImageBuffer(image2)
ClsColor 0,255,0
Cls
SetBuffer ImageBuffer(image3)
ClsColor 0,0,255
Cls

;set active layer and image
Global currentlayer.layers = layer1.layers
Global currentimage        = image1

SetBuffer BackBuffer()
ClsColor 255,255,255
Repeat
	Cls	
		;render layers
		If Last layers <> Null
			layer.layers = Last layers
			Repeat
				;render this layers tiles
				If layer\s <> Null
					tile.tiles = layer\s
					Repeat
						DrawImage tile\image,tile\x,tile\y
						
						If tile = layer\e Exit
						tile = After tile
					Forever
				End If
				
				If layer = First layers Exit
				layer = Before layer
			Forever
		End If
		
		;user interface
		x = MouseX()
		y = MouseY()
		If x > 160
			DrawImage currentimage,x,y
			If MouseHit(1)
				CreateTile(currentimage,x,y,currentlayer)
			End If
		End If
		
		If KeyHit(2) currentlayer = layer1
		If KeyHit(3) currentlayer = layer2
		If KeyHit(4) currentlayer = layer3
			
		If KeyHit(200) MoveLayerUp(currentlayer)
		If KeyHit(208) MoveLayerDown(currentlayer)
			
		If KeyHit(57)
			Select currentimage
				Case image1
					currentimage = image2
				Case image2
					currentimage = image3
				Case image3
					currentimage = image1
			End Select
		End If
		
		Color 0,0,0
		Rect 0,0,160,480,1
		Color 255,255,255
		Text 2,2,"left click to place"
		Text 2,16,"a tile."
		Text 2,40,"up to move layer up"
		Text 2,64,"down to move layer"
		Text 2,78,"down"
		Text 2,102,"1,2,3 to change"
		Text 2,116,"the current layer"
		Text 2,140,"space to change"
		Text 2,154,"tile image"
		
		Text 10,178,"current tile"
		DrawImage currentimage,40,192
		
		y = 244
		For layer.layers = Each layers
			If layer = layer1
				If layer = currentlayer
					Color 0,128,0
					Rect 2,y,156,20,1
					Color 0,55,0
					Rect 2,y,156,20,0
					Color 255,255,255
					Text 4,y,"layer 1"
				Else
					Color 128,128,128
					Rect 2,y,156,20,1
					Color 55,55,55
					Rect 2,y,156,20,0
					Color 0,0,0
					Text 4,y,"layer 1"
				End If
			ElseIf layer = layer2
				If layer = currentlayer
					Color 0,128,0
					Rect 2,y,156,20,1
					Color 0,55,0
					Rect 2,y,156,20,0
					Color 255,255,255
					Text 4,y,"layer 2"
				Else
					Color 128,128,128
					Rect 2,y,156,20,1
					Color 55,55,55
					Rect 2,y,156,20,0
					Color 0,0,0
					Text 4,y,"layer 2"
				End If
			ElseIf layer = layer3
				If layer = currentlayer
					Color 0,128,0
					Rect 2,y,156,20,1
					Color 0,55,0
					Rect 2,y,156,20,0
					Color 255,255,255
					Text 4,y,"layer 3"
				Else
					Color 128,128,128
					Rect 2,y,156,20,1
					Color 55,55,55
					Rect 2,y,156,20,0
					Color 0,0,0
					Text 4,y,"layer 3"
				End If
			End If
			y = y + 20
		Next
	Flip
	WaitTimer(timer)
Until KeyDown(1)



MSW(Posted 2004) [#3]
Erm...how about a double linked list?

type tile

field image
field x
field y

field next.tile
field previous.tile

end type

Then your layers only need to function as "pointers" into individual groupings of tiles...and each tile "points" to both the next and previous tile with in the group.

sorta like this (trying to sorta diagram it out):


first these are the layers:

Layer.tile(1) -> tile 707
Layer.tile(2) -> tile 53


now for the tiles:

tile 707.previous -> Null
tile 707.next -> tile 811



tile 811.previous -> tile 707
tile 811.next -> tile 12



tile 12.previous -> tile 811
tile 12.next -> Null


tile 53.previous -> Null
tile 53.Next -> Null



(the numbers 707,811,12 are just there it identify specific tiles in the diagram, showing that the tile order is specificly controlled by the next/previous links)...notice that the first tile...the one that the layer points to...the 'previous' link value is null...and the last tile in the grouping, the 'next' link value is null...in this way you can easily check if you have reached the top or bottom of a layer grouping (and if both 'previous' and 'next' links are Null...then there it only one tile in grouping)

So useing the above...say you wanted to move tile 811 from the first to the second layer grouping...a function like this could work (assumeing that layer() is a global array )

Function movetile( me.tile, laynumber%)

;temp variables used to store the previous/next pointers
tempprevious.tile = me.previous
tempnext.tile = me.next

;swap out the previous/next pointers with the linked tiles
;effectively this removes the "me" tile from the layer group

if tempprevious then tempprevious.next = tempnext

if tempnext then tempnext.previous = tempprevious


;now set "me" previous to null (indicateing it is the first tile in a layer grouping)

me.previous = null

;then set "me" next to the first tile in the layer grouping

me.next = layer(laynumber)

;then set that tile (the first tile in the laynumber grouping)
;'previous' value to "me"

if layer(layernumber) then layer(laynumber).previous = me

;and finaly set "me" at the top of the leyer grouping

layer(laynumber) = me

end function


It's kinda hard to explain linked lists (and I likely screwed up the blitz programing syntax for types up there..which won't help)...but I hope that helps


Adam Bossy(Posted 2004) [#4]
Thanks for the replies!

So, when drawing the tiles, should I use

For t.tile = each tile
DrawImage t\image
Next

Or would something like this be necessary for it to draw correctly?

me.tile = First tile
While me <> Null
DrawImage me\image
me = me.next
Wend


MSW(Posted 2004) [#5]
The second option would be the way to go for the linked list method I outlined above.

The problem with conditional level checking as shown by Skn[ac] in the code above is that you end up checking all the tiles multiple times...this isn't bad when you have a small over all number of them...but say you have 10,000 (a 100 by 100 array)...and 17 of them are on the first layer, 45 are on the second, 9,000 on the third, and 54 on the fourth...you end up checking all 10,000 tiles 4 times per update...and thus in the first layer alone with only 17 tiles to draw, means you are wasteing time checking the remaining 9983 tiles...sure this is pretty fast, but you are wasteing time on checking instead of processing other things (like AI, etc..)

However there are simple ways to speed even that up...bounds checking...for each layer you would record the minimum and maximum array locations that include the specific layer in question...

for example if the map is 100 tiles wide by 100 tiles tall you could have something like this for each layer at map load:

Layer1_min_X = 100
Layer1_min_Y = 100
Layer1_max_X = 0
Layer1_max_y = 0

then run through the tiles in the array...if the tile is on the particular layer then check if the array X/Y location is within those bounds:

For X = 0 to 100
For Y = 0 to 100

;obviously do this for each layer
If map.tile(x,y)\layer = 1 then
If X < level1_min_X then level1_min_x = x
If y < level1_min_y then level1_min_y = y
If X > level1_max_X then level1_max_x = x
If y > level1_max_y then level1_max_y = y
end if

next
next


then when you draw, you can do it as Skn[ac] showed except for each layer you set up the for/next loops to start at minX/Y to maxX/Y...something like:

For X = Level1_min_X to Level1_max_X
For Y = Level1)min_Y to Level1_max_Y

if map.tile(x,y)\layer = 1 then draw tile

next
next

this will only have you checking for the correct layer in an area of the map that is certain to have tiles in it...meaning you arn't wasteing time checking for the correct layer in potentialy larger areas of the map that won't have tiles to draw right now...

However the linked list method is more efficent in that it directly points to the tiles that need to be drawn.