heygleeson / godot-ldtk-importer

LDTK Importer for Godot 4
MIT License
141 stars 13 forks source link

Is there a way to import "level.ldtkl" files? #3

Closed Granshmeyr closed 4 weeks ago

Granshmeyr commented 1 year ago

I'm new to game development and LDtk. I want to try implementing lazy loading to optimize performance. I also want to try storing a lot of levels in a GridVania world that I can use as a pool to generate a dungeon layout.

It seems I have to load every level at once to utilize an imported world.ldtk scene. But LDtk has the option to export individual level files. Is there a way I can utilize these files or implement some other logic so I can achieve lazy loading, etc.?

The best thing I could think of is injecting a VisibleOnScreenEnabler2D underneath every level node, but that seems very janky and still requires the loading of every level at once.

Cammin commented 1 year ago

Does Godot support making variants of an imported hierarchy? Potentially you may be able to make a scene variant of just that level. But I'm also not completely sure myself if that's possible 🙂

Granshmeyr commented 1 year ago

Does Godot support making variants of an imported hierarchy? Potentially you may be able to make a scene variant of just that level. But I'm also not completely sure myself if that's possible 🙂

It appears to be possible, but it doesn't allow me to delete children so the end result is the same. After some thought I think the only solution is to add the functionality in myself by reusing some logic from level.gd but I don't want to devote time to that right now.

heygleeson commented 1 year ago

One of the constraints with the way the importer is architectured at the moment, is that it resolves EntityRef fields as NodePaths when importing/reimporting. These get resolved as the last step, and assumes the whole World Scene is organised in the way it gets built - levels are siblings that can access eachother in the SceneTree. If they can't be accessed, or get removed, then that NodePath reference gets broken.

This is also why it's currently not possible yet to load LDTKLevel (.ldtkl) files as separate scenes (amongst other reasons). It's something I plan on fixing, but it does require a bit of work.

In the meantime, the best solution I can offer is to write a level post-import script to add a VisibleOnScreenEnabler2D, supplying it the level.size:

@tool

func post_import(level: LDTKLevel) -> LDTKLevel:
    var visible_enabler = VisibleOnScreenEnabler2D.new()
    visible_enabler.rect = Rect2(Vector2.ZERO, level.size)
    level.add_child(visible_enabler)
    return level

There are other, more creative ways of writing post-import scripts to get around the current limitations of the importer - but I wouldn't necessarily recommend doing them.

How would you handle when to load/unload levels if you treated them as separate scenes?

Granshmeyr commented 1 year ago

I was planning on using LDtk's "world" feature simply as a way of organizing levels (in my case TileMaps)—externally from Godot entirely. And also for the little bit of extra metadata that comes from having the levels be in a world such as world(X, Y) and worldDepth. So I was planning on loading levels in my current project using traditional level transition areas and then specifying resource paths and optionally coordinates to instantiate the new level. That's just how I was doing it before when I saved my TileMaps as scenes.

Personally it seems counterintuitive creating three different "worlds" just to have three different TileMaps, functionally. I never thought the "world" feature would correspond directly to the game's design other than being a handy way of structuring TileMap relations in LDtk itself.

heygleeson commented 1 year ago

Worlds can include multiple layers of tilemaps as well as entities - there are many reasons to keep them separate (toggle visibility, pause processing per level, etc.). There are also many ways to approach using LDtk, and the importer is tailored for users to quickly get up and running, and tries to cover as much of the spec as possible.

For example, you can make level transition area 'entities' in LDtk that point to other locations/areas on the map - these currently would get resolved by the importer as NodePaths. It would be possible in the future to convert them into some kind of hybrid ResourcePath/NodePath resolver (that allows for saving out individual Level scenes), but it would involve a bit of effort and planning to get that working in a nice, intuitive way.

I appreciate the feedback! I want to know more about how people intend to use this plugin.

grebhun commented 11 months ago

Hi there,

I had a similar request regarding individual level files with this importer! I saw your comment above about hearing what others use this importer for, so I figured I'd add my two cents.

Similar to @Granshmeyr , I intend on using LDtk to create a metroidvania-style map (similar to what's outlined here where the world itself is comprised of many separate "levels" (rooms) that are all interconnected (but not really).

Once imported, I'd like to have separate scenes for each of my levels. Then, I can add custom player spawn points and transition triggers to each level that map to the next proper "level" (this is something that I would also like to be able to add simply in LDtk, but haven't explored Entities enough yet to know the limitations). Basically, if the player is in the scene for "Level_0" and moves out of that area, the game will fade out, load up the scene for "Level_1" and spawn the player at the correct location in that scene before fading back in.

Basically the Hollow Knight way of doing a map. And I believe in Godot, this way requires having separate scenes for each room/level. I don't think this importer currently supports anything like that, but is definitely something I would love to see!

This is an amazing plugin, thanks for all of your work!

heygleeson commented 11 months ago

I imagined this would be the kind of workflow, but an issue I haven't found a good solution for yet is resolving EntityRefs that point outside of the current level/scene.

Say you had an Entity - let's say a Door - which contained an EntityRef that pointed to a Door in another Level. What the importer does currently is resolve this ref into a NodePath since it currently assumes this can be resolved (and it can!).

When this exists outside of the scene, and/or can't be resolved, then it gets interesting.

It would be nice if the importer included functionality to handle EntityRefs for you, so it can be aware of levels entering/exiting the tree.

What I haven't really figured out is: Which node should be responsible for doing the work of loading levels and resolving any new references. There are a few ideas:

I feel like a solution may combine some/all of these things, but it has been tricky to not force one way of doing things.

I am curious to know how people think this should be handled.

grebhun commented 11 months ago

Yeah, that does sound like a challenging problem. I can't say I completely follow since I'm still a bit unfamiliar with how this importer works under the hood (specifically with Entities), but I may just need to take a closer look at the code. How would each of those options affect the use of the plugin at the end of the day? Option 3 obviously makes you rely on an autoload which some people don't love, but sounds simple to me.

I haven't even started using Entities in my LDtk maps yet, so I'm not quite sure about how those work. I'll try to take a look at those in the meantime. Cheers!

gummycookie commented 9 months ago

I think the global manager idea is enticing. I pretty much did this to some degree w/ monogame. I also have the same use case given my game will have a lot of large levels.

I may just work around it by have N number of ldtk projects haha

gummycookie commented 9 months ago

okay this is what I've used for now.. feel free to use this, your mileage my vary .

A couple of warnings with this:

@tool
var Util = preload("res://addons/ldtk-importer/src/util/util.gd")

func post_import(level: LDTKLevel) -> LDTKLevel:
    #reset to origin
    level.position = Vector2.ZERO;

    for child in level.get_children():
        Util.recursive_set_owner(child, level, level)

    var levelScene = PackedScene.new();
    levelScene.pack(level); 

    var err = ResourceSaver.save(levelScene, "res://levels/" +  level.name + ".tscn");

    if err:
        print(err)
    return level
grebhun commented 9 months ago

This is a great solution for now! Much appreciated, I didn't realize it could be so simple. And for me, it still seems to reimport automatically using this script as well. Thanks!