How to update the cells of a TileMap in Godot in batch?

Godot Version

Godot-4.2-mono

Question

Currently, I only found the TileMap.SetCell() method in the documentation. I currently need to update approximately 260,000 cells in 1Tick, which is far too slow. I would like to know if there is any way to optimize it?

Currently there is not. TileMap is not made for this use case. Best you can do is put all the needed updates in an array and only do so many each frame, leave the rest for the next frame, so it doesn’t block the ui, but if you want to do it in one tick it’s gonna block and be slow. The other alternative is not using TileMap at all, which is possible, faster, but loses all the nicer features, obviously. Pick your poison, there’s no magic solution.

private Vector2I _size;
private Texture2D _texture2D;
private Sprite2D[] _sprites;
private Node2D[] _chunks;

public override void _Ready() {
			Node2D node2D = new Node2D();
			CanvasGroup canvasGroup = new CanvasGroup();
			_sprites = new Sprite2D[_size.X * _size.Y];
			_chunks = new Node2D[_size.X / 16];
			for (int i = 0; i < _sprites.Length; i++) {
				_sprites[i] = new Sprite2D();
				_sprites[i].Texture = _texture2D;
				_sprites[i].Position = new Vector2I(i / _size.X * 32, i % _size.Y * 32);
			}

			int spriteIndex = 0;
			for (int i = 0; i < _chunks.Length; i++) {
				_chunks[i] = new Node2D();
				for (int j = 0; j < (_sprites.Length / _chunks.Length); j++) {
					_chunks[i].AddChild(_sprites[spriteIndex]);
					spriteIndex++;
				}

				canvasGroup.AddChild(_chunks[i]);
			}

			node2D.ProcessThreadGroup = ProcessThreadGroupEnum.SubThread;
			node2D.AddChild(canvasGroup);
			AddChild(node2D);
		}

		public override void _PhysicsProcess(double delta) {
			// _world.Progress();
			for (int i = 0; i < _chunks.Length; i++) {
				Vector2 globalPosition = _chunks[i].GlobalTransform.Origin;
				Rect2 cameraRect = this.GetViewport().GetCamera2D().GetViewportRect();
				if (globalPosition.X >= cameraRect.Position.X && globalPosition.X <= cameraRect.End.X &&
				    globalPosition.Y >= cameraRect.Position.Y && globalPosition.Y <= cameraRect.End.Y) {
					_chunks[i].Visible = true;
				} else {
					_chunks[i].Visible = false;
				}
			}
		}

Currently, I use sprite2d for rendering. If there are approximately 260,000 nodes to be rendered, the delay will be very large. I have written a rudimentary culling system.I don’t know how to write it optimally.

You can render it as a single quad using the texture atlas and a shader only. You just upload the tilemap array as a data texture and use that to figure out what tile each pixel belongs to. If you can’t figure it out, I have a test project I can dig around for, I think.

I don’t quite understand how I should use texture atlas to render

Have you ever written a shader?

I haven’t been exposed to this

After saying I went try make an example and realized I had done this on my own Racket-based engine, so I tried to recreate it in godot and found out godot does not yet support buffer textures… which makes this unreasonably hard to do, so I am afraid you are stuck with either regular TileMap or the barely more efficient RenderServer approach. You’re just gonna have to figure out how to deal with the slow process without making it faster. Sorry.

I used MeshInstance2D to implement it. It renders 100,000 meshes well, but if it renders 250,000 meshes, it performs poorly. I’m wondering if there’s any way to optimize it

RenderingServer functions might be better for 2D than meshes, but if you’re rendering 250k quads at once on screen you’re asking a lot of the engine. It’d work fine it it was a batched mesh, I guess? Hard to tell without trying.

The official document does not have very detailed content, which makes me very frustrated

	[Export] public Button Button;
		private Image _image;
		private Texture2D _texture1;
		private Texture2D _texture2;
		private Vector2I _size;
		private Rid[] _rids;

		public override void _Ready() {
			_size = new Vector2I(512, 512);
			_rids = new Rid[_size.X * _size.Y];
			_texture1 = GD.Load<Texture2D>("res://texture/tile/tile-1.png");
			_texture2 = GD.Load<Texture2D>("res://texture/tile/tile-2.png");
			
			Engine.PhysicsTicksPerSecond = 10;
			Rect2 textRect = new Rect2(_texture1.GetSize(), _texture1.GetSize());
			for (int i = 0; i < _rids.Length; i++) {
				_rids[i] = RenderingServer.CanvasItemCreate();
				RenderingServer.CanvasItemSetParent(_rids[i], GetCanvasItem());
				RenderingServer.CanvasItemAddTextureRect(_rids[i], new Rect2(_texture1.GetSize(), _texture1.GetSize()),
					_texture1.GetRid());
				Transform2D transform = Transform2D.Identity.Translated(new Vector2(32 * i / 512, 32 * i % 512));
				RenderingServer.CanvasItemSetTransform(_rids[i], transform);
			}

			Button.Pressed += () => {
				RenderingServer.TextureReplace(_texture1.GetRid(), _texture2.GetRid());
			};
		}

This is the code I used for RenderingServer. I don’t know how to replace the texture for each RID, or just draw a region of a Texture.

canvas_item_add_texture_rect_region()
So in your case it’d be

RenderingServer.CanvasItemAddTextureRectRegion(
	//Canvas RID
	_rids[i],
	//Position in the world of the tile
	new Rect2(world_position, world_size),
	//Texture atlas RID
	_texture1.GetRid(),
	//Position of the region in the atlas
	new Rect2(atlas_tile_position, atlas_tile_size));

I think that’s all you’d need? This lets you draw the tiles from one atlas. If it’s more than one atlas, you could merge them or have them in an array/dict.

I don’t think so, it has serious performance loss