Tilemap extension: updates overwrite original definition

I recently updated my next game to use the Tilemap extension (https://github.com/microsoft/pxt-tilemaps). The ability to store tilemaps (representing levels) in an array made the code for loading a level much nicer!

I think I just noticed a problem, though.

After loading a level, I spawn the characters on spawn tiles, and then delete the spawn tiles.

If a character falls (the only way to die), I have code to restart the current level. This code was working in the pre-tilemap extension form, but my program is now behaving as my spawn tile update mutated the original tilemap definition stored in the array (it seems like the characters are spawning in a default location).

Is that a fair guess as to what’s going wrong? Is there a way to copy/clone a tilemap to be able to make changes to it without overwriting the original?

I’m not sure what the “right” behavior is here. Should these changes be persistent? That behavior is sometimes useful for things, like removing a coin from a room after it has been collected. @darzu what do you think?

In any case, both of the APIs should match.

Should these changes be persistent? That behavior is sometimes useful for things…

I completely agree. While I was surprised by the behavior, I can understand its utility.

However, an additional block to support loading a copy of a tilemap if you want to opt-out of mutating the original would be a really nice addition.

Great question. I can think of many useful situations where you’d want either behavior. When resetting a level you want a fresh copy, when moving around a persistent world you probably want mutation.

So I think we should expose both to the user. The question becomes which is default and which is opt-in.

For mutate by default:
A copy function definitely works and would naturally make mutation the default with a mechanism for avoiding mutating the originals.

For copy-on-load by default:
To have mutation be opt-in… I can’t think of a great way to express this to the user.
Maybe “set current tilemap () with mutation” but that’s awkward, and doesn’t easily allow mix and matching both scenarios.
Maybe we’d want some sort of expanded notation of “current” tilemaps that go beyond just the one. Then you could transition between “current” tilemaps and state would be preserved, but if you removed tilemaps from the “current” set you’d reset all that state.

I’m leaning toward mutate by default with an additional copy tilemap function. I think the more common case is that users would expect mutation to persist and that you reset state by resetting the game.

Hmm, I ran into difficulty trying to implement “copy tilemap”.

Here’s the conundrum: tilemaps can have “connections” to other tilemaps that are bidirectional and remembered with a number. So when you copy a tilemap, does it have the same connections as the old tilemap?
Let’s say you have two maps: A and B
A and B are connected by ID 7
Copy A to get C
Mutate A a bunch
Now on B, load the connected map by ID 7. Does that load A (with mutations) or C (before mutations) ?
@richard any thoughts?

I’m thinking there isn’t a clean way to handle connection copy. Making them one-directional doesn’t help. I think we’ll need to copy without connections. The question becomes how do we communicate this to the user. “copy (tilemap) without connections” might work but it’s a little verbose.

:+1: This seems reasonable.

Looking at the blocks involved, I don’t think I’d expect a connection to be copied when a tilemap was cloned. Especially since connections don’t seem to involve actual tiles on the tilemap; they appear to just be named links between two tilemap instances.

1 Like

@richard, I can’t find a clean way to clone a tilemap. Tilemap.data() is protected. Thoughts?

This should not be an opt-in behavior, it should just be one way or the other. I’m voting that mutations should stick around.

As for cloning the tilemap, this shouldn’t work but totally does:

(game.currentScene().tileMap as any).data

Yes that’s what I meant above: I also vote by default mutations are how things should work, but we also provide a way to copy tilemaps so you can avoid mutation i.e. “opt-out” of mutation.

I’ll try that for cloning for now, but I’ll also see about making a change to common packages to better support cloning for future PXT releases.

I don’t think you should have two clones. Otherwise we’d have to keep track of the unedited tilemap in addition to the edited one and that could be a lot of memory overhead.

I don’t want two clones either. I meant I’ll implement clone for now using (game.currentScene().tileMap as any).data but also I think we should change common packages so we don’t need a cast to any and then in the future we can modify that one clone to remove the cast.

@darzu then what do you mean by having an opt-out?

Tilemaps will mutate by default. If a user doesn’t want a tilemap to mutate then they’ll need to copy the tilemap and load the copy instead. The process of copying a tilemap is what I meant by “opting out” of mutation.

@jacob_c, I updated pxt-tilemaps (v1.7.0) with a new “copy” block.
image

You can try it here: https://makecode.com/_JgrEmxavKPVH (“A” load a copy, “B” mutates)

Thanks for pointing out this gap!

1 Like

Thanks! Seems to work great in my game, too.

1 Like