Saving TileMap Data?

Monkey Forums/Monkey Programming/Saving TileMap Data?

Amon(Posted 2012) [#1]
What would be the best method of saving my TileMap Data to file? I noticed Load/SaveString but alas I'm at a loss on how to proceed.

I'm not using any frameworks just simple coding it it to suit my current game.

So far here is what I have, just the mere simple basics: for anyone interested.

Strict

Import mojo

Function Main:Int()
	New SimpleMap()
	Return 1
End Function


Class SimpleMap Extends App
	Global tiles:Image
	Global tileArray:Int[][]
	Global OffX:Float
	Global offY:Float
	Global Tilesize:Int
	Global currentTile:Int
	
	Method OnCreate:Int()
		OffX = 10
		offY = 10
		currentTile = 0
		Tilesize = 64
		tiles = LoadImage("tiles.png", 32, 32, 10, Image.DefaultFlags)
		tileArray = AllocateArray(12, 10)
		For Local x:Int = 0 until 12
			For Local y:Int = 0 until 10
				tileArray[x][y] = -1
			Next
		Next
		
		SetUpdateRate(60)
		Return 1
	End Method
	
	Method OnUpdate:Int()
		If KeyHit(KEY_SPACE) Then SimpleMap.currentTile+=1
		If SimpleMap.currentTile > 9 Then SimpleMap.currentTile = 0	
		Return 1
	End Method
	
	Method OnRender:Int()
		Cls
		DrawText("CurrentTile="+SimpleMap.currentTile, 0, 0, 0, 0)
		For Local x:Int = 0 until 12
			For Local y:Int = 0 until 10
				If tileArray[x][y] =  0 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 0)
				If tileArray[x][y] =  1 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 1)
				If tileArray[x][y] =  2 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 2)
				If tileArray[x][y] =  3 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 3)
				If tileArray[x][y] =  4 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 4)
				If tileArray[x][y] =  5 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 5)
				If tileArray[x][y] =  6 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 6)
				If tileArray[x][y] =  7 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 7)
				If tileArray[x][y] =  8 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 8)
				If tileArray[x][y] =  9 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 9)
			Next
		Next
		PlaceTile()
		Return 1
	End Method
	
End Class

Function PlaceTile:Int()
	If MouseDown(MOUSE_LEFT)
		If MouseX() - SimpleMap.OffX > 0 And MouseX() - SimpleMap.OffX < SimpleMap.Tilesize * 12 - 32
			If MouseY() - SimpleMap.offY > 0 And MouseY() - SimpleMap.offY < SimpleMap.Tilesize * 10 - 32
				SimpleMap.tileArray[MouseX() / SimpleMap.Tilesize][MouseY() / SimpleMap.Tilesize] = SimpleMap.currentTile
			EndIf
		EndIf
	EndIf
	If MouseDown(MOUSE_RIGHT) Then SimpleMap.tileArray[MouseX() / SimpleMap.Tilesize][MouseY() / SimpleMap.Tilesize] = -1
	Return 0
End Function

Function AllocateArray:Int[][]( i:Int, j:Int)
    Local arr:Int[][] = New Int[i][]
    For Local ind:Int = 0 Until i
        arr[ind] = New Int[j]
    Next
    Return arr		
End



Grey Alien(Posted 2012) [#2]
I just added saving to my game jam game last weekend and added some things to help with that to my minigame framework. Here's some code from my project that may help you if you adapt it.

	Method SaveLevel()
		#If TARGET="glfw" Then
			Local levelString:TFileString = New TFileString
			For Local block:TBlock = Eachin level.blockList
				
				levelString.AddParam("tag",block.tag)
				levelString.AddParam("x",Int(block.x))
				levelString.AddParam("y",Int(block.y))
				levelString.AddLineBreak()
			Next
			Print levelString.output
			os.SaveString(levelString.output,ccGetSourceDataPath()+levelNumber+".txt")

		#End		
	End Method

	Method Load:Void(levelNumber:Int)
		Local levelString:TFileString = New TFileString

		#If TARGET="glfw" Then
			'No need to add loading protection as it fails safely.
			If DEBUG Then
				'Use source data folder as that's what the Editor writes to and reads from.
				levelString.output=os.LoadString(ccGetSourceDataPath()+levelNumber+".txt")
			Else
				levelString.output=os.LoadString(ccGetDataPath()+levelNumber+".txt")
			Endif
		#Else
			'This will load from the data folder instead (I believe it gets compiled into the exe)
			levelString.output=app.LoadString(levelNumber+".txt")			
		#End
		
		levelString.RemoveLineBreaks()
	End Method

Function ccGetSourceDataPath:String()
	'Returns the source data path e.g. mygame.data.  This is not the data path in the final build.
	#If HOST = "winnt" Then
		Return RealPath(ExtractDir(AppPath())+"/../../../../"+APP_NAME+".data/")+"/"
	#Elseif HOST = "macos" Then
'		Return RealPath(ExtractDir(AppPath())+"/../../../../../../../../"+os.StripAll(AppPath())+".data/") 'This doesn't work because the exe is called monkeygame
		Return RealPath(ExtractDir(AppPath())+"/../../../../../../../../"+APP_NAME+".data/")+"/" 'APP_NAME is a constant I have defined
	#End
	Return ""
End Function

Function ccGetDataPath:String()
	'Returns the data path of the final build
	#If HOST = "winnt" Then
		Return ExtractDir(AppPath())+"/data/"
	#Elseif HOST = "macos" Then
		Return ExtractDir(AppPath())+"/../Resources/data/"
	#End
	Return ""
End Function

'/////////////////////////////////////////////////
'TFileString
'/////////////////////////////////////////////////
	
Class TFileString
	Public
	Field output:String
	Field delimiter:String = ","
	
	Public
	
	Method AddParam(paramName:String, paramValue:String)
		output+=paramName+"="+paramValue+delimiter
	End Method
	
	Method AddLineBreak()
		output+=String.FromChars([13,10]) 
	End Method
	
	Method GetParam:String()
		'Throw away the param name and = sign.
		output = output[output.Find("=")+1..]
		Local pos:Int = output.Find(delimiter)
		Local result:String = Left(output, pos)
		output = output[pos+1..]
		Return result
	End Method
	
	Method RemoveLineBreaks:Void()
		output = output.Replace(String.FromChars([13,10]) , "")
	End Method
End Class




Grey Alien(Posted 2012) [#3]
I should mention several things:

- You can't save/load data to folders on your computer with HTML5/Flash so when I use my editor I compile a GLFW build.
- The Editor in my game is only available in Debug mode and then it loads and saves directly from the source data folder. The one named mygame.data. This way whenever I make a new Flash/HTML 5 build the data folder is already up-to-date with the latest levels.
- However when the game is running in release mode it just loads data in from the build's data folder which is the correct place to load data from.


Amon(Posted 2012) [#4]
Don't get it. :)

I mean I get some of it. Seeing my code in the OP, how would I implement it in that?

After testing for a while I got as far as nowhere which is near can't get it to work... :)

[edit]Specifically I got SaveString working but it only saves the first tile. It most definitely does not work the same way as BMAX.


muddy_shoes(Posted 2012) [#5]
The code you've provided doesn't seem to try saving anything. If you post what you've got then maybe someone can spot where you're going wrong.


Amon(Posted 2012) [#6]
Here is the updated code with my SaveString hack implemented:

Strict

Import mojo
Import os

Function Main:Int()
	New SimpleMap()
	Return 1
End Function


Class SimpleMap Extends App
	Global tiles:Image
	Global tileArray:Int[][]
	Global OffX:Float
	Global offY:Float
	Global Tilesize:Int
	Global currentTile:Int
	
	Method OnCreate:Int()
		OffX = 10
		offY = 10
		currentTile = 0
		Tilesize = 64
		tiles = LoadImage("tiles.png", 32, 32, 10, Image.DefaultFlags)
		tileArray = AllocateArray(12, 10)
		For Local x:Int = 0 until 12
			For Local y:Int = 0 until 10
				tileArray[x][y] = -1
			Next
		Next
		
		SetUpdateRate(60)
		Return 1
	End Method
	
	Method OnUpdate:Int()
		If KeyHit(KEY_SPACE) Then SimpleMap.currentTile+=1
		If SimpleMap.currentTile > 9 Then SimpleMap.currentTile = 0	
		SaveMap()
		Return 1
	End Method
	
	Method OnRender:Int()
		Cls
		DrawText("CurrentTile="+SimpleMap.currentTile, 0, 0, 0, 0)
		For Local x:Int = 0 until 12
			For Local y:Int = 0 until 10
				If tileArray[x][y] =  0 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 0)
				If tileArray[x][y] =  1 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 1)
				If tileArray[x][y] =  2 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 2)
				If tileArray[x][y] =  3 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 3)
				If tileArray[x][y] =  4 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 4)
				If tileArray[x][y] =  5 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 5)
				If tileArray[x][y] =  6 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 6)
				If tileArray[x][y] =  7 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 7)
				If tileArray[x][y] =  8 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 8)
				If tileArray[x][y] =  9 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 9)
			Next
		Next
		PlaceTile()
		Return 1
	End Method
	
End Class

Function SaveMap:Int()
	If KeyHit(KEY_F1)
		Local tempArr:Int[][]
		Local Fileout:String
		Local tempData:String
		Local level:Int
		level = -1
		Fileout = "map"
		tempArr = AllocateArray(12, 10)
		For Local x:Int = 0 until 12
			For Local y:Int = 0 until 10
				tempArr[x][y] = SimpleMap.tileArray[x][y]
			Next
		Next
		For Local x2:Int = 0 until 12
			For Local y2:Int = 0 until 10
				tempData = String(tempArr[x2][y2])
			Next
		Next
		SaveString( tempData, ExtractDir(AppPath()+"/data/"+Fileout+""+level+".txt"))
	EndIf
	Return 0
End Function

Function PlaceTile:Int()
	If MouseDown(MOUSE_LEFT)
		If MouseX() - SimpleMap.OffX > 0 And MouseX() - SimpleMap.OffX < SimpleMap.Tilesize * 12 - 32
			If MouseY() - SimpleMap.offY > 0 And MouseY() - SimpleMap.offY < SimpleMap.Tilesize * 10 - 32
				SimpleMap.tileArray[MouseX() / SimpleMap.Tilesize][MouseY() / SimpleMap.Tilesize] = SimpleMap.currentTile
			EndIf
		EndIf
	EndIf
	If MouseDown(MOUSE_RIGHT) Then SimpleMap.tileArray[MouseX() / SimpleMap.Tilesize][MouseY() / SimpleMap.Tilesize] = -1
	Return 0
End Function

Function AllocateArray:Int[][]( i:Int, j:Int)
    Local arr:Int[][] = New Int[i][]
    For Local ind:Int = 0 Until i
        arr[ind] = New Int[j]
    Next
    Return arr		
End



muddy_shoes(Posted 2012) [#7]
From a brief glance, this:

For Local x2:Int = 0 until 12
    For Local y2:Int = 0 until 10
        tempData = String(tempArr[x2][y2])
    Next
Next


...is going to always only include one cell because you're just replacing the previous tempData value every time. If you use "tempData += " you'll append the values.


Amon(Posted 2012) [#8]
Yep that seems to have solved it. Thanks!

I put another line after the first tempData: tempData+="," so that each value of the Array is separated by a comma.

I've got it working nicely now and can proceed.

Thanks Again!


Amon(Posted 2012) [#9]
Is there a non confusing way of loading .txt files from the data directory?

Looking at the example Grey posted it's too confusing for me to figure out.


Raz(Posted 2012) [#10]
Local aString:String = LoadString("file.txt")


Then do what ever is needed with the string. :)


Raz(Posted 2012) [#11]
Also, for what it's worth... the following part of code could be quicker.
If tileArray[x][y] =  0 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 0)
If tileArray[x][y] =  1 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 1)
If tileArray[x][y] =  2 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 2)
If tileArray[x][y] =  3 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 3)
If tileArray[x][y] =  4 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 4)
If tileArray[x][y] =  5 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 5)
If tileArray[x][y] =  6 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 6)
If tileArray[x][y] =  7 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 7)
If tileArray[x][y] =  8 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 8)
If tileArray[x][y] =  9 Then DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 9)


you could either do a select statement so it's...

Select tileArray[x][y]
Case 0
    DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 0)
Case 1
    DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, 1)
...
End


or unless things change drastically, just do...

DrawImage(tiles, OffX + x * Tilesize, offY + y * Tilesize, 0, 2, 2, tileArray[x][y])



Grey Alien(Posted 2012) [#12]
Sorry Amon. It was confusing for me too when I was figuring it out :-)


Paul - Taiphoz(Posted 2012) [#13]
possibly this will help.

The code bellow loads my Terminal 2 projects level's into the level array, the save code is kinda the same just saving out the data as a comma delimited file.

I would show you that as well, but it's not written in monkey, I did the editor in Max.

The file itself is just 213,123,1,23,12,3,1,31,23, with each number representing a tile for the level.

It's kinda crude, does not store any additional information about each tile, or collisions, but then I didnt need it to so.

Anyway. hope it helps.

[monkeycode]
#Rem
sumary:Load Stage
Loads the given stage, parsing in wave data, and
loading in the correct tile sets and tile data.
#End
Method LoadStage(stage:Int=1)

CacheAliens(stage)

Self.StageSpeed=.7
Local tmpImage:Image
Self.offsetx=-16

Local LevelIN:String
LevelIN = LoadString("Stage_"+String(stage)+".txt")

Local SpawnIN:String
SpawnIN = LoadString("Stage_"+String(stage)+"_wave.txt")

Self.BackGround = game.images.LoadAnim("backgrounds/stage_"+stage+".png", 480,200,6,tmpImage,True)
Self.BackGroundOffset = 0
Self.BackGroundStep = 0

game.images.LoadAnim("StageTiles_"+stage+".png", 128, 128, 80, tmpImage,True)
Self.StageTiles = game.images.Find("StageTiles_"+String(stage))

Local loop:Int = 0
For Local NewGrid:String = Eachin LevelIN.Split(",")
If loop<500
Self.grid[loop]=Int(NewGrid)
loop+=1
End If
Next

Local bit:Int=1
Local id:Int=0
Local trig:Int=0
Local cnt:Int=0
Local unit:Int =0
For Local value:String = Eachin SpawnIN.Split(",")

Print "Bit ="+bit


Select bit
Case 1
unit=Int(value)
Case 2
id=Int(value)
Case 3
trig=Int(value)
Case 4
cnt=Int(value)
End Select

If bit=4 Then
AddWave(unit,id,trig,cnt)
Print "Adding Wave ["+id+","+trig+","+cnt+"]"
bit=0
End If

bit=bit+1
bit = bit Mod 5

Next


Local loop2:Int = 0
For Local NewGrid:String = Eachin LevelIN.Split(";")
'gets a line ended by a ;
'split by , to get each spawn data.


Next

End Method
[/monkeycode]

EDIT, just fixing the bbocde and switching it to monkeycode.