Posted: 21st Dec 2022 0:09
This post will tell you how I used LDtk to make a level for Project Jupiter

If like me you want to add Enums, Entities as well as tile sets with a simple layering system LDtk lets you do that in a productive way. And adding all this into your AppGameKit project is easy too, with the LDtkFileType.load(loadFileStr) command. But before programming you will need to make your level in LDtk.

When you are happy with your creation, set the 'Save Levels To Separate Files' box and ensure you are saving the .ldtk extension.

For project Jupiter I made a really simple file with three layers, a wall layer, a fill layer, and an entity layer. The wall layer is where the ship cannot go and has physics applied, the fill layer is just a sprite with no physics and the entity is where I want the enemies to be.

The next part of the import into AppGameKit requires the use of TYPES. If you don't know about setting up types you can read up all about them here: https://www.appgamekit.com/documentation/guides/types_001.htm All we need to do here is set up a TYPE and then latter we read our .json file into it so the data is stored in memory ready to be used. The code below shows the TYPE I made for Project Jupiter.
+ Code Snippet
// File: ldTKmulti.agc
// Created: 22-06-27

TYPE TileSetUidT

tileSetUid as integer
x as integer
y as integer
w as integer
h as integer

ENDTYPE

TYPE realEditorValuesT

id as string
params as string[]

ENDTYPE

TYPE fieldInstanceT

__identifier as string
__value as string
__type as string
__tile as TileSetUidT
defUid as integer
realEditorValues as realEditorValuesT[]

ENDTYPE

TYPE layerInstancesT

__identifier as string
__type as string
__cWid as integer
__cHei as integer
_gridSize as integer
__opacity as integer
__pxTotalOffsetX as integer
__pxTotalOffsetY as integer
__tilesetRelPath as string
iid as string
levelId as integer
layerDefUid as integer
pxOffsetX as integer
pxOffsetY as integer
visible as integer
optionalRules as integer[]
intGridCsv as integer[]
autoLayerTiles as integer[]
seed as integer
overrideTilesetUid as string
gridTiles as gridTilesT[]
entityInstances as entityInstanceT[]

ENDTYPE

TYPE entityInstanceT

__identifier as string
__grid as integer[2]
__pivot as integer[2]
__tags as string[]
__tile as TileSetUidT
__smartColor as string
iid as string
width as float
height as float
defUid as integer
px as float[2]
fieldInstances as fieldInstanceT[]

ENDTYPE

TYPE gridTilesT

px as integer[2]
src as integer[2]
f as integer
t as integer
d as integer

ENDTYPE

TYPE ldTKrootT

__header__ as HeaderMultiT
identifier as string
iid as string
uid as integer
worldX as integer
worldY as integer
worldDepth as integer
pxWid as integer
pxHei as integer
__bgColor as string
bgColor as string
useAutoIdentifier as integer
bgRelPath as integer
bgPos as integer
bgPivotX as float
bgPivotY as float
__smartColor as string
__bgPos as integer
externalRelPath as string
fieldInstances as fieldInstanceT[]
layerInstances as layerInstancesT[]
__neighbours as levelIidT[]

ENDTYPE

TYPE levelIidT

levelIid as string
levelUid as integer
dir as string

ENDTYPE

TYPE HeaderMultiT

fileType as string
app as string
doc as string
schema as string
appAuthor as string
appVersion as string
url as string

ENDTYPE

TYPE levelDataT

ID as integer
levelData as ldTKrootT[]

ENDTYPE

TYPE staticSpritesT

spriteID as integer
iid as string
status as integer
name as string
ori as string
doorTween as integer
doorOpenPlaying as integer
doorIsLocked as integer
doorLockCode as integer

ENDTYPE


and for those who like to read .json files, here it is from the .LDtk save.

+ Code Snippet
{
	"__header__": {
		"fileType": "LDtk Project JSON",
		"app": "LDtk",
		"doc": "https://ldtk.io/json",
		"schema": "https://ldtk.io/files/JSON_SCHEMA.json",
		"appAuthor": "Sebastien 'deepnight' Benard",
		"appVersion": "1.1.3",
		"url": "https://ldtk.io"
	},
	"jsonVersion": "1.1.3",
	"appBuildId": 458364,
	"nextUid": 13,
	"identifierStyle": "Capitalize",
	"worldLayout": "Free",
	"worldGridWidth": 256,
	"worldGridHeight": 256,
	"defaultLevelWidth": 256,
	"defaultLevelHeight": 256,
	"defaultPivotX": 0,
	"defaultPivotY": 0,
	"defaultGridSize": 16,
	"bgColor": "#40465B",
	"defaultLevelBgColor": "#696A79",
	"minifyJson": false,
	"externalLevels": true,
	"exportTiled": false,
	"simplifiedExport": false,
	"imageExportMode": "None",
	"pngFilePattern": null,
	"backupOnSave": false,
	"backupLimit": 10,
	"levelNamePattern": "Level_%idx",
	"tutorialDesc": null,
	"flags": [],
	"defs": { "layers": [
		{
			"__type": "Tiles",
			"identifier": "Tiles",
			"type": "Tiles",
			"uid": 5,
			"gridSize": 32,
			"guideGridWid": 0,
			"guideGridHei": 0,
			"displayOpacity": 1,
			"inactiveOpacity": 1,
			"hideInList": false,
			"hideFieldsWhenInactive": false,
			"pxOffsetX": 0,
			"pxOffsetY": 0,
			"parallaxFactorX": 0,
			"parallaxFactorY": 0,
			"parallaxScaling": true,
			"requiredTags": [],
			"excludedTags": [],
			"intGridValues": [],
			"autoTilesetDefUid": 4,
			"autoRuleGroups": [],
			"autoSourceLayerDefUid": null,
			"tilesetDefUid": 4,
			"tilePivotX": 0,
			"tilePivotY": 0
		},
		{
			"__type": "Tiles",
			"identifier": "Fill",
			"type": "Tiles",
			"uid": 6,
			"gridSize": 32,
			"guideGridWid": 0,
			"guideGridHei": 0,
			"displayOpacity": 1,
			"inactiveOpacity": 1,
			"hideInList": false,
			"hideFieldsWhenInactive": false,
			"pxOffsetX": 0,
			"pxOffsetY": 0,
			"parallaxFactorX": 0,
			"parallaxFactorY": 0,
			"parallaxScaling": true,
			"requiredTags": [],
			"excludedTags": [],
			"intGridValues": [],
			"autoTilesetDefUid": 4,
			"autoRuleGroups": [],
			"autoSourceLayerDefUid": null,
			"tilesetDefUid": 4,
			"tilePivotX": 0,
			"tilePivotY": 0
		},
		{
			"__type": "Entities",
			"identifier": "Entities",
			"type": "Entities",
			"uid": 9,
			"gridSize": 32,
			"guideGridWid": 0,
			"guideGridHei": 0,
			"displayOpacity": 1,
			"inactiveOpacity": 0.6,
			"hideInList": false,
			"hideFieldsWhenInactive": true,
			"pxOffsetX": 0,
			"pxOffsetY": 0,
			"parallaxFactorX": 0,
			"parallaxFactorY": 0,
			"parallaxScaling": true,
			"requiredTags": [],
			"excludedTags": [],
			"intGridValues": [],
			"autoTilesetDefUid": null,
			"autoRuleGroups": [],
			"autoSourceLayerDefUid": null,
			"tilesetDefUid": null,
			"tilePivotX": 0,
			"tilePivotY": 0
		}
	], "entities": [
		{
			"identifier": "Entity",
			"uid": 11,
			"tags": [],
			"width": 32,
			"height": 32,
			"resizableX": false,
			"resizableY": false,
			"keepAspectRatio": false,
			"tileOpacity": 1,
			"fillOpacity": 1,
			"lineOpacity": 1,
			"hollow": false,
			"color": "#94D9B3",
			"renderMode": "Rectangle",
			"showName": true,
			"tilesetId": null,
			"tileId": null,
			"tileRenderMode": "FitInside",
			"tileRect": null,
			"nineSliceBorders": [],
			"maxCount": 0,
			"limitScope": "PerLevel",
			"limitBehavior": "MoveLastOne",
			"pivotX": 0,
			"pivotY": 0,
			"fieldDefs": [
				{
					"identifier": "StaticEnemy",
					"__type": "LocalEnum.StaticEnemy",
					"uid": 12,
					"type": "F_Enum(10)",
					"isArray": false,
					"canBeNull": false,
					"arrayMinLength": null,
					"arrayMaxLength": null,
					"editorDisplayMode": "Hidden",
					"editorDisplayPos": "Above",
					"editorAlwaysShow": false,
					"editorCutLongValues": true,
					"editorTextSuffix": null,
					"editorTextPrefix": null,
					"useForSmartColor": false,
					"min": null,
					"max": null,
					"regex": null,
					"acceptFileTypes": null,
					"defaultOverride": null,
					"textLanguageMode": null,
					"symmetricalRef": false,
					"autoChainRef": true,
					"allowOutOfLevelRef": true,
					"allowedRefs": "OnlySame",
					"allowedRefTags": [],
					"tilesetUid": null
				}
			]
		}
	], "tilesets": [
		{
			"__cWid": 4,
			"__cHei": 8,
			"identifier": "MainTiles_Tile_32",
			"uid": 4,
			"relPath": "media/Tiles/MainTiles-Tile_32.png",
			"embedAtlas": null,
			"pxWid": 128,
			"pxHei": 256,
			"tileGridSize": 32,
			"spacing": 0,
			"padding": 0,
			"tags": [],
			"tagsSourceEnumUid": null,
			"enumTags": [],
			"customData": [],
			"savedSelections": [],
			"cachedPixelData": {
				"opaqueTiles": "11111111111111111111111111111000",
				"averageColors": "f666f666f666f666f555f555f555f555f444f444f445f444f334f767f767f555f767f767f777f445f445f445f445f666f666f666f666f666f666000000000000"
			}
		}
	], "enums": [{ "identifier": "StaticEnemy", "uid": 10, "values": [{ "id": "Turret", "tileId": null, "color": 0, "__tileSrcRect": null }], "iconTilesetUid": null, "externalRelPath": null, "externalFileChecksum": null, "tags": [] }], "externalEnums": [], "levelFields": [] },
	"levels": [
		{
			"identifier": "Level_0",
			"iid": "affe5db0-02f0-11ed-84a4-29f5b78754e6",
			"uid": 0,
			"worldX": 0,
			"worldY": 32,
			"worldDepth": 0,
			"pxWid": 6976,
			"pxHei": 1120,
			"__bgColor": "#696A79",
			"bgColor": null,
			"useAutoIdentifier": true,
			"bgRelPath": null,
			"bgPos": null,
			"bgPivotX": 0.5,
			"bgPivotY": 0.5,
			"__smartColor": "#ADADB5",
			"__bgPos": null,
			"externalRelPath": "Project Jupiter/Level_0.ldtkl",
			"fieldInstances": [],
			"layerInstances": null,
			"__neighbours": []
		}
	],
	"worlds": []
}


Now, here's the bit that got me stuck for a bit and I had to do a bit of head scratching to figure out what was going on. If you take a look at the .json file, it starts with a { and ends with a }. But AGK's .json command e.g. loadmyData.load(thisfile$) wants the file to be bracketed in a [ and a ]. This is because loading .json files into AppGameKit is an iterative function and the command expects it to be denoted as such. The solution is then to add [ at the file start and a ] at the file end. And this can be done easily in code:
+ Code Snippet
function copyLevelDataFromLDtk()

sourceFileStr as string
destFileStr as string

for i = 0 to NUMBER_OF_LEVELS

sourceFileStr = "raw:H:\AGK\AGK Games\Project Jupiter\Project Jupiter\Level_" + str(i) + ".ldtkl"
destFileStr = "raw:H:\AGK\AGK Games\Project Jupiter\media\LevelData\Level_" + str(i) + ".ldtkl"

copyFile(sourceFileStr,destFileStr)

next i

endfunction


function copyFile(fileName$, newName$)
    // open the two files
    sourceFile = OpenToRead(fileName$)
    destFile = OpenToWrite(newName$)

    // while we are not at the end of the source file
    WriteLine(destFile,"[")
    while FileEOF(sourceFile) = 0
        // write the next byte in the source file to the dest file
        // by going through it byte by byte, this function will be able to copy any file, regardless of format
        WriteLine(destFile, ReadLine(sourceFile))
    endwhile
	WriteLine(destFile,"]")
    // close the files
    CloseFile(sourceFile)
    CloseFile(destFile)
endfunction


Next load in your tile sets. How you do this is up to you. I loaded them into an array using subimage.txt file as described here : https://www.appgamekit.com/documentation/Reference/Image/LoadSubImage.htm

With everything now ready, to add our level into our game we need to parse the data we loaded from .json file add in the tiles and put them into the world in the correct places. The code I used to do this is shown below.
+ Code Snippet
function loadAllLevelData3()   /// this function loads levelData.ldtk and stores the information into array2's. There is one array2 for each of the layer.

tempWalls as gridTilesT[]  // use gridTilesT
tempFill as gridTilesT[]
tempEntities as entityInstanceT[] // use entityInstanceT

tempLevel as ldTKrootT[]
tempLevelData as levelDataT

loadFileStr as string

for i = 0 to NUMBER_OF_LEVELS // We will want to do this for every level. But for this example N

loadFileStr = "LevelData/Level_" + str(i) + ".ldtkl"

tempLevel.load(loadFileStr) // this is the AGK command to load our level data. Remember to ensure to add [] at the start and end of the file!

tempLevelData.ID = i
tempLevelData.levelData = tempLevel

levelDataMain.insert(tempLevelData)

next i

for i = 0 to NUMBER_OF_LEVELS

for s = 0 to 2

if levelDataMain[i].levelData[0].layerInstances[s].__identifier = "Tiles"
	tempWalls = levelDataMain[i].levelData[0].layerInstances[s].gridTiles
	Walls.insert(tempWalls)
endif

if levelDataMain[i].levelData[0].layerInstances[s].__identifier = "Fill"
	tempFill = levelDataMain[i].levelData[0].layerInstances[s].gridTiles
	Fill.insert(tempFill)
endif

if levelDataMain[i].levelData[0].layerInstances[s].__identifier = "Entities"
	tempEntities = levelDataMain[i].levelData[0].layerInstances[s].entityInstances
	Entities.insert(tempEntities)
endif
next s
next i

endfunction

function displayLevelDIM(level) /// this function takes information from the data created by the .json file, loads the correct tile and makes a sprite for it in the correct place

tempSprite as staticSpritesT
	
	for i = 0 to Walls[level].length 
		subImageToUse = Walls[level,i].t
		tempSprite.spriteID = createSprite(subImageList[subImageToUse])
		SetSpriteShape(tempSprite.spriteID,3)		
		SetSpritePosition(tempSprite.spriteID, Walls[level,i].px[0], Walls[level,i].px[1])
		SetSpriteDepth(tempSprite.spriteID, 805)
		SetSpritePhysicsOn(tempSprite.spriteID,1)
		SetSpriteCategoryBit(tempSprite.spriteID,1,0)
		SetSpriteCategoryBit(tempSprite.spriteID,3,1)
		SetspriteColor(tempSprite.spriteID,128,128,128,255)
		staticSpritesList.insert(tempSprite)
	next i

	for i = 0 to Fill[level].length 
		subImageToUse = Fill[level,i].t
		tempSprite.spriteID = createSprite(subImageList[subImageToUse])
		SetSpriteShape(tempSprite.spriteID,3)		
		SetSpritePosition(tempSprite.spriteID, Fill[level,i].px[0], Fill[level,i].px[1])
		SetSpriteDepth(tempSprite.spriteID, 805)
		//SetSpritePhysicsOn(tempSprite.spriteID,1)
		//SetSpriteCategoryBit(tempSprite.spriteID,1,0)
		//SetSpriteCategoryBit(tempSprite.spriteID,3,1)
		SetspriteColor(tempSprite.spriteID,128,128,128,255)
		staticSpritesList.insert(tempSprite)
	next i


	for i = 0 to Entities[level].length
		
		if Entities[level,i].__identifier = "Entity"
			//doorPlacement(Entities[level,i].px[0],Entities[level,i].px[1],Entities[level,i].fieldInstances[0].__value,Entities[level,i].fieldInstances[1].__value,Entities[level,i].fieldInstances[2].__value, Entities[level,i].iid)
			turretPlacement(Entities[level,i].px[0],Entities[level,i].px[1])
		//Message(str(Entities.length))	
		endif

next i
endfunction



LDtk has much to offer in terms of 2D level design and it is worth a look. Because the data can be saved as a .json then to read and parse the data is easy, just remember to add your '[]' on the file or AppGameKit can't load it. Also, LDtk records all the information into the .json so this is accessible from the .json structure, if you know where to look. Everything you need is well documented : https://ldtk.io/json/

If you want to play Project Jupiter it is here for download : https://dewarinversion.itch.io/project-jupiter
Posted: 21st Dec 2022 20:08
I've never heard of ldtk before, how would you say it stacks up against Tiled?
Posted: 2nd Jan 2023 16:04
LDtk is highly capable in terms of workflow. Choosing between Tiled and LDtk I prefer the latter. There are still some features available in Tiled which aren't in LDtk like the History Editor and the Tile Collision Editor. However, LDtk is a work in progress and 1.2.0 has just been released (today) which adds the following :
1. Auto Layer Rules Assistant.
2. Reworked Entity Field Visuals
3. Auto Layer Rules Remapping.
4. Icons for Entities (I like this one!)
5. In Editor Enum Tag Display.
6. Custom Script Commands.

You can find out more here : https://ldtk.io/showcase/

Image screenshot of editor used to create multiple levels for Red Tonya https://dewarinversion.itch.io/red-tonya
Posted: 6th Feb 2023 22:47
Hey i try to use this editor for one of my projects.
So i started to rewrite your code to use references instead of globals for example:

+ Code Snippet
function LDtk_LoadGridTiles(levelDataMain ref as LDtk_levelDataT[], GridTiles ref as LDtk_gridTilesT[][], Identifier$)
	tempTiles as LDtk_gridTilesT[]
	
	for LevelID = 0 to levelDataMain.length
		for s = 0 to levelDataMain[LevelID].levelData[0].layerInstances.length
			if levelDataMain[LevelID].levelData[0].layerInstances[s].__identifier = Identifier$
			    tempTiles = levelDataMain[LevelID].levelData[0].layerInstances[s].gridTiles
			    GridTiles.insert(tempTiles)
			endif
		next s
	next LevelID
endfunction

And then
+ Code Snippet
function LDtk_CreateTiles(SubImageList as integer[], GridTiles ref as LDtk_gridTilesT[][], LevelID as integer)
	SpriteList as LDtk_staticSpritesT[]
	TempSprite as LDtk_staticSpritesT
        
    for TileID = 0 to GridTiles[LevelID].length
        SubImageID=GridTiles[LevelID,TileID].t
        TempSprite.SpriteID = CreateSprite(SubImageList[SubImageID])
        SetSpritePosition(TempSprite.SpriteID, GridTiles[LevelID,TileID].px[0], GridTiles[LevelID,TileID].px[1])
        LDtk_FlipSprite(TempSprite.SpriteID, GridTiles[LevelID,TileID].f)
        SetSpriteDepth(TempSprite.SpriteID, 11)
        SetSpritePhysicsOn(TempSprite.SpriteID,1)
        SetSpriteGroup(TempSprite.SpriteID,1)
		SetSpriteCategoryBits(TempSprite.SpriteID,%11)
		SetSpriteCollideBits(TempSprite.SpriteID,%01)
		SpriteList.insert(TempSprite)
    next TileID
endfunction SpriteList

And the flip sprite function looks like this:
+ Code Snippet
function LDtk_FlipSprite(SpriteID as integer, State as integer)
	local Horizontal as integer
	local Vertical as integer
	Horizontal=1&&State>0
	Vertical=2&&State>0
	SetSpriteFlip(SpriteID,Horizontal,Vertical)
endfunction

And i think we could easily read the relative path of the sprite sheet and generate the subimage file from the information given.
Write a neat function that gives us a Subimage List and maybe even create a function that can do it all in one go.

I also have a question:
Why did you use this ?
+ Code Snippet
type LDtk_levelDataT
	ID as integer
	levelData as LDtk_ldTKrootT[]
endtype
....
levelDataMain.insert(tempLevelData)

Isn't a list of LDtk_ldTKrootT enough ?

And what are your thoughts on using LDtk and the Virtualresolution in AppGameKit?
I normally use a virtual resolution of 100x100 and GetScreenBoundsLeft ect. to position stuff so i have to post process the positions of the tiles as we can't scale the tiles in the editor right ?

I didnt really dive into LDtk much but it looks very promising and wanted to give it a try, so thanks for sharing and pointing us to it.