TilemapRenderer.cs 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Microsoft.Xna.Framework;
  5. using Microsoft.Xna.Framework.Graphics;
  6. namespace MonoGame.Extended.Tilemaps.Rendering;
  7. /// <summary>
  8. /// High-performance tilemap renderer using GraphicsDevice directly.
  9. /// </summary>
  10. /// <remarks>
  11. /// <para>
  12. /// This renderer provides optimal performance by rendering tiles using vertex and index buffers.
  13. /// Supports layer grouping to merge multiple layers into a single draw call, significantly
  14. /// reducing draw call overhead for maps with many layers.
  15. /// </para>
  16. /// <para>
  17. /// Layer groups can be defined dynamically and updated at runtime. Groups are rebuilt
  18. /// automatically when modified. For best performance with static maps, define groups
  19. /// once during initialization.
  20. /// </para>
  21. /// <para>
  22. /// Use <see cref="TilemapSpriteBatchRenderer"/> for simpler integration with SpriteBatch-based
  23. /// code at the cost of some performance.
  24. /// </para>
  25. /// </remarks>
  26. public sealed class TilemapRenderer : IDisposable
  27. {
  28. private readonly GraphicsDevice _graphicsDevice;
  29. private BasicEffect _effect;
  30. private Tilemap _tilemap;
  31. private RenderMode _defaultRenderMode;
  32. private bool _isDisposed;
  33. // Multi-tilemap world support (for LDtk GridVania layouts)
  34. private readonly List<WorldTilemap> _worldTilemaps;
  35. private bool _isWorldMode;
  36. // State management for SpriteBatch mixing
  37. private BlendState _savedBlendState;
  38. private SamplerState _savedSamplerState;
  39. private RasterizerState _savedRasterizerState;
  40. private DepthStencilState _savedDepthStencilState;
  41. // Layer groups and models
  42. private readonly Dictionary<string, LayerGroup> _layerGroups;
  43. private readonly Dictionary<TilemapLayer, string> _layerToGroup;
  44. private readonly List<LayerModel> _layerModels;
  45. // Drawing state
  46. private bool _isDrawing;
  47. private Matrix _viewMatrix;
  48. private Matrix _projectionMatrix;
  49. /// <summary>
  50. /// Initializes a new instance of the <see cref="TilemapRenderer"/> class.
  51. /// </summary>
  52. /// <param name="graphicsDevice">The graphics device.</param>
  53. /// <exception cref="ArgumentNullException">Thrown if graphicsDevice is null.</exception>
  54. public TilemapRenderer(GraphicsDevice graphicsDevice)
  55. {
  56. _graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice));
  57. _effect = new BasicEffect(_graphicsDevice)
  58. {
  59. TextureEnabled = true,
  60. VertexColorEnabled = false
  61. };
  62. _layerGroups = new Dictionary<string, LayerGroup>();
  63. _layerToGroup = new Dictionary<TilemapLayer, string>();
  64. _layerModels = new List<LayerModel>();
  65. _worldTilemaps = new List<WorldTilemap>();
  66. // Default to Merged mode (will be confirmed by benchmarks in Week 3)
  67. _defaultRenderMode = RenderMode.Merged;
  68. }
  69. /// <summary>
  70. /// Gets the current default rendering mode.
  71. /// </summary>
  72. public RenderMode DefaultRenderMode
  73. {
  74. get
  75. {
  76. ThrowIfDisposed();
  77. return _defaultRenderMode;
  78. }
  79. }
  80. /// <summary>
  81. /// Gets the names of all defined layer groups.
  82. /// </summary>
  83. public IReadOnlyList<string> LayerGroups => new List<string>(_layerGroups.Keys);
  84. /// <summary>
  85. /// Loads a tilemap for rendering.
  86. /// </summary>
  87. /// <param name="tilemap">The tilemap to load.</param>
  88. /// <exception cref="ArgumentNullException">Thrown if tilemap is null.</exception>
  89. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  90. public void LoadTilemap(Tilemap tilemap)
  91. {
  92. ThrowIfDisposed();
  93. if (tilemap == null)
  94. throw new ArgumentNullException(nameof(tilemap));
  95. // Unload previous tilemap if any
  96. UnloadTilemap();
  97. _tilemap = tilemap;
  98. _isWorldMode = false;
  99. // Build vertex/index buffers for all tile layers
  100. BuildLayerModels();
  101. }
  102. /// <summary>
  103. /// Loads multiple tilemaps as a seamless world (for LDtk GridVania layouts).
  104. /// </summary>
  105. /// <param name="tilemaps">The collection of tilemaps with world position data.</param>
  106. /// <remarks>
  107. /// <para>
  108. /// This method is designed for LDtk GridVania worlds where each level has world coordinates
  109. /// stored in properties (LDtk_WorldX, LDtk_WorldY, LDtk_WorldDepth).
  110. /// </para>
  111. /// <para>
  112. /// Use <see cref="DrawWorld(OrthographicCamera, int)"/> to render specific WorldDepth layers.
  113. /// The game controls which depth is visible based on gameplay (e.g., WorldDepth 0 for overworld,
  114. /// WorldDepth 1 for building interiors).
  115. /// </para>
  116. /// <para>
  117. /// World coordinates are read from tilemap properties. If properties are not found, the
  118. /// tilemap is placed at (0,0) with depth 0.
  119. /// </para>
  120. /// </remarks>
  121. /// <exception cref="ArgumentNullException">Thrown if tilemaps is null.</exception>
  122. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  123. public void LoadWorld(IEnumerable<Tilemap> tilemaps)
  124. {
  125. ThrowIfDisposed();
  126. if (tilemaps == null)
  127. throw new ArgumentNullException(nameof(tilemaps));
  128. // Unload any previous data
  129. UnloadTilemap();
  130. _isWorldMode = true;
  131. // Build layer models for each tilemap with world position
  132. foreach (Tilemap tilemap in tilemaps)
  133. {
  134. WorldTilemap worldTilemap = new WorldTilemap
  135. {
  136. Tilemap = tilemap,
  137. WorldPosition = GetWorldPosition(tilemap),
  138. WorldDepth = GetWorldDepth(tilemap)
  139. };
  140. // Build models for this tilemap's layers
  141. foreach (TilemapLayer layer in tilemap.Layers)
  142. {
  143. if (layer is TilemapTileLayer tileLayer)
  144. {
  145. LayerModel model = BuildLayerModelForWorld(tileLayer, tilemap, worldTilemap.WorldPosition);
  146. if (model != null)
  147. {
  148. worldTilemap.LayerModels.Add(model);
  149. }
  150. }
  151. }
  152. _worldTilemaps.Add(worldTilemap);
  153. }
  154. }
  155. /// <summary>
  156. /// Gets the world position for a tilemap from its properties.
  157. /// </summary>
  158. /// <param name="tilemap">The tilemap to get world position from.</param>
  159. /// <returns>The world position in pixels, or (0,0) if not found.</returns>
  160. private Vector2 GetWorldPosition(Tilemap tilemap)
  161. {
  162. Vector2 position = Vector2.Zero;
  163. if (tilemap.Properties.TryGetValue("LDtk_WorldX", out TilemapPropertyValue worldX))
  164. {
  165. position.X = worldX.AsInt();
  166. }
  167. if (tilemap.Properties.TryGetValue("LDtk_WorldY", out TilemapPropertyValue worldY))
  168. {
  169. position.Y = worldY.AsInt();
  170. }
  171. return position;
  172. }
  173. /// <summary>
  174. /// Gets the world depth for a tilemap from its properties.
  175. /// </summary>
  176. /// <param name="tilemap">The tilemap to get world depth from.</param>
  177. /// <returns>The world depth (z-order), or 0 if not found.</returns>
  178. private int GetWorldDepth(Tilemap tilemap)
  179. {
  180. if (tilemap.Properties.TryGetValue("LDtk_WorldDepth", out TilemapPropertyValue worldDepth))
  181. {
  182. return worldDepth.AsInt();
  183. }
  184. return 0; // Default depth
  185. }
  186. /// <summary>
  187. /// Builds a layer model with world position offset applied.
  188. /// </summary>
  189. /// <param name="tileLayer">The tile layer to build buffers for.</param>
  190. /// <param name="tilemap">The parent tilemap containing this layer.</param>
  191. /// <param name="worldPosition">The world position offset for this tilemap.</param>
  192. /// <returns>A LayerModel containing the vertex/index buffers, or null if the layer is empty.</returns>
  193. private LayerModel BuildLayerModelForWorld(TilemapTileLayer tileLayer, Tilemap tilemap, Vector2 worldPosition)
  194. {
  195. List<VertexPositionTexture> vertices = new List<VertexPositionTexture>();
  196. List<ushort> indices = new List<ushort>();
  197. Texture2D currentTexture = null;
  198. foreach ((int x, int y, TilemapTile tile) in tileLayer.GetTiles())
  199. {
  200. // Check if tileset exists for this tile's global ID
  201. TilemapTileset tileset = tilemap.Tilesets.GetTilesetForGid(tile.GlobalId);
  202. if (tileset == null)
  203. continue; // Skip tiles with no tileset (can happen in world mode)
  204. int localId = tile.GlobalId - tileset.FirstGlobalId;
  205. if (currentTexture == null)
  206. currentTexture = tileset.Texture;
  207. Rectangle sourceRect = tileset.GetTileRegion(localId);
  208. Point tilePos = tilemap.TileToWorldPosition(x, y);
  209. // Apply world position offset + layer offset + tileset offset
  210. Vector2 position = new Vector2(tilePos.X, tilePos.Y) + worldPosition + tileLayer.Offset + tileset.TileOffset;
  211. AddTileQuad(vertices, indices, position, tileLayer.TileWidth, tileLayer.TileHeight,
  212. sourceRect, tile.FlipFlags, tileset.Texture);
  213. }
  214. if (vertices.Count == 0)
  215. return null;
  216. return CreateLayerModel(vertices.ToArray(), indices.ToArray(), currentTexture);
  217. }
  218. /// <summary>
  219. /// Builds vertex and index buffers for all tile layers in the tilemap.
  220. /// </summary>
  221. private void BuildLayerModels()
  222. {
  223. foreach (TilemapLayer layer in _tilemap.Layers)
  224. {
  225. // Only build models for tile layers (not object/image/group layers)
  226. if (layer is TilemapTileLayer tileLayer)
  227. {
  228. LayerModel model = BuildLayerModel(tileLayer);
  229. if (model != null)
  230. {
  231. _layerModels.Add(model);
  232. }
  233. }
  234. }
  235. }
  236. /// <summary>
  237. /// Builds a vertex and index buffer for a single tile layer.
  238. /// </summary>
  239. /// <param name="tileLayer">The tile layer to build buffers for.</param>
  240. /// <returns>A LayerModel containing the vertex/index buffers, or null if the layer is empty.</returns>
  241. private LayerModel BuildLayerModel(TilemapTileLayer tileLayer)
  242. {
  243. // Collect vertices and indices for all tiles in this layer
  244. List<VertexPositionTexture> vertices = new List<VertexPositionTexture>();
  245. List<ushort> indices = new List<ushort>();
  246. // Group tiles by texture to minimize state changes
  247. // For now, we'll build a single model per layer and assume single texture
  248. // Multi-texture support will be added in Week 2 with layer groups
  249. Texture2D currentTexture = null;
  250. foreach ((int x, int y, TilemapTile tile) in tileLayer.GetTiles())
  251. {
  252. // Get tileset and local ID for this tile
  253. int localId = tile.GetLocalId(_tilemap.Tilesets, out TilemapTileset tileset);
  254. if (tileset == null)
  255. continue; // Skip tiles with no tileset
  256. // Track the texture (for now we assume all tiles in a layer use the same texture)
  257. if (currentTexture == null)
  258. currentTexture = tileset.Texture;
  259. // Get source rectangle from tileset
  260. Rectangle sourceRect = tileset.GetTileRegion(localId);
  261. // Calculate world position for this tile
  262. Point worldPos = _tilemap.TileToWorldPosition(x, y);
  263. // Apply layer offset
  264. Vector2 position = new Vector2(worldPos.X, worldPos.Y) + tileLayer.Offset;
  265. // Apply tileset tile offset
  266. position += tileset.TileOffset;
  267. // Add the tile quad (4 vertices, 6 indices)
  268. AddTileQuad(vertices, indices, position, tileLayer.TileWidth, tileLayer.TileHeight,
  269. sourceRect, tile.FlipFlags, tileset.Texture);
  270. }
  271. // If no tiles were added, return null
  272. if (vertices.Count == 0)
  273. return null;
  274. // Create GPU buffers
  275. return CreateLayerModel(vertices.ToArray(), indices.ToArray(), currentTexture);
  276. }
  277. /// <summary>
  278. /// Adds a tile quad to the vertex and index lists.
  279. /// </summary>
  280. /// <param name="vertices">The vertex list to append to.</param>
  281. /// <param name="indices">The index list to append to.</param>
  282. /// <param name="position">The world position of the tile's top-left corner.</param>
  283. /// <param name="width">The width of the tile in pixels.</param>
  284. /// <param name="height">The height of the tile in pixels.</param>
  285. /// <param name="sourceRect">The source rectangle in the texture.</param>
  286. /// <param name="flipFlags">The flip transformation flags.</param>
  287. /// <param name="texture">The texture to sample UVs from.</param>
  288. private void AddTileQuad(List<VertexPositionTexture> vertices, List<ushort> indices,
  289. Vector2 position, int width, int height,
  290. Rectangle sourceRect, TilemapTileFlipFlags flipFlags, Texture2D texture)
  291. {
  292. // Calculate the four corners of the quad
  293. Vector3 topLeft = new Vector3(position.X, position.Y, 0);
  294. Vector3 topRight = new Vector3(position.X + width, position.Y, 0);
  295. Vector3 bottomLeft = new Vector3(position.X, position.Y + height, 0);
  296. Vector3 bottomRight = new Vector3(position.X + width, position.Y + height, 0);
  297. // Calculate texture coordinates with flip flags
  298. Vector2[] uvs = CalculateTextureCoordinates(sourceRect, flipFlags, texture);
  299. // Add vertices (top-left, top-right, bottom-left, bottom-right)
  300. ushort vertexOffset = (ushort)vertices.Count;
  301. vertices.Add(new VertexPositionTexture(topLeft, uvs[0]));
  302. vertices.Add(new VertexPositionTexture(topRight, uvs[1]));
  303. vertices.Add(new VertexPositionTexture(bottomLeft, uvs[2]));
  304. vertices.Add(new VertexPositionTexture(bottomRight, uvs[3]));
  305. // Add indices for two triangles (counter-clockwise winding)
  306. // Triangle 1: top-left, top-right, bottom-left
  307. indices.Add(vertexOffset);
  308. indices.Add((ushort)(vertexOffset + 1));
  309. indices.Add((ushort)(vertexOffset + 2));
  310. // Triangle 2: top-right, bottom-right, bottom-left
  311. indices.Add((ushort)(vertexOffset + 1));
  312. indices.Add((ushort)(vertexOffset + 3));
  313. indices.Add((ushort)(vertexOffset + 2));
  314. }
  315. /// <summary>
  316. /// Calculates texture coordinates for a tile quad, applying flip transformations.
  317. /// </summary>
  318. /// <param name="sourceRect">The source rectangle in the texture.</param>
  319. /// <param name="flipFlags">The flip transformation flags.</param>
  320. /// <param name="texture">The texture to calculate UVs from.</param>
  321. /// <returns>An array of 4 UV coordinates: [top-left, top-right, bottom-left, bottom-right].</returns>
  322. /// <remarks>
  323. /// UV coordinates are inset by 0.5 texels to prevent texture bleeding and visible seams
  324. /// between tiles. This is a common technique to avoid sampling pixels outside the intended
  325. /// tile boundary when texture filtering is applied.
  326. /// </remarks>
  327. private Vector2[] CalculateTextureCoordinates(Rectangle sourceRect, TilemapTileFlipFlags flipFlags, Texture2D texture)
  328. {
  329. // Calculate texel size for UV inset
  330. float texelWidth = 1.0f / texture.Width;
  331. float texelHeight = 1.0f / texture.Height;
  332. // Inset by 0.5 texels to prevent sampling outside tile boundaries
  333. // This eliminates visible seams between tiles
  334. float insetU = texelWidth * 0.5f;
  335. float insetV = texelHeight * 0.5f;
  336. // Normalize coordinates to 0-1 range with inset applied
  337. float left = (sourceRect.Left + 0.5f) / texture.Width;
  338. float right = (sourceRect.Right - 0.5f) / texture.Width;
  339. float top = (sourceRect.Top + 0.5f) / texture.Height;
  340. float bottom = (sourceRect.Bottom - 0.5f) / texture.Height;
  341. // Apply horizontal flip
  342. if ((flipFlags & TilemapTileFlipFlags.FlipHorizontally) != 0)
  343. {
  344. (left, right) = (right, left);
  345. }
  346. // Apply vertical flip
  347. if ((flipFlags & TilemapTileFlipFlags.FlipVertically) != 0)
  348. {
  349. (top, bottom) = (bottom, top);
  350. }
  351. // Apply diagonal flip (90° rotation + horizontal flip)
  352. // This is the tricky one: it swaps axes and requires rearranging UVs
  353. if ((flipFlags & TilemapTileFlipFlags.FlipDiagonally) != 0)
  354. {
  355. // Diagonal flip means: rotate 90° clockwise, then flip horizontally
  356. // This results in swapping x and y coordinates
  357. // UV mapping becomes: [bottom-left, top-left, bottom-right, top-right]
  358. return new Vector2[]
  359. {
  360. new Vector2(left, bottom), // top-left → bottom-left
  361. new Vector2(left, top), // top-right → top-left
  362. new Vector2(right, bottom), // bottom-left → bottom-right
  363. new Vector2(right, top) // bottom-right → top-right
  364. };
  365. }
  366. // Standard UV mapping: [top-left, top-right, bottom-left, bottom-right]
  367. return new Vector2[]
  368. {
  369. new Vector2(left, top),
  370. new Vector2(right, top),
  371. new Vector2(left, bottom),
  372. new Vector2(right, bottom)
  373. };
  374. }
  375. /// <summary>
  376. /// Creates GPU vertex and index buffers from vertex/index arrays.
  377. /// </summary>
  378. /// <param name="vertices">The vertex array.</param>
  379. /// <param name="indices">The index array.</param>
  380. /// <param name="texture">The texture for this layer.</param>
  381. /// <returns>A LayerModel containing the GPU resources.</returns>
  382. private LayerModel CreateLayerModel(VertexPositionTexture[] vertices, ushort[] indices, Texture2D texture)
  383. {
  384. // Create vertex buffer
  385. VertexBuffer vertexBuffer = new VertexBuffer(
  386. _graphicsDevice,
  387. typeof(VertexPositionTexture),
  388. vertices.Length,
  389. BufferUsage.WriteOnly);
  390. vertexBuffer.SetData(vertices);
  391. // Create index buffer
  392. IndexBuffer indexBuffer = new IndexBuffer(
  393. _graphicsDevice,
  394. IndexElementSize.SixteenBits,
  395. indices.Length,
  396. BufferUsage.WriteOnly);
  397. indexBuffer.SetData(indices);
  398. return new LayerModel
  399. {
  400. VertexBuffer = vertexBuffer,
  401. IndexBuffer = indexBuffer,
  402. Texture = texture,
  403. PrimitiveCount = indices.Length / 3
  404. };
  405. }
  406. /// <summary>
  407. /// Unloads the current tilemap and disposes all GPU resources.
  408. /// </summary>
  409. public void UnloadTilemap()
  410. {
  411. ThrowIfDisposed();
  412. // Dispose all layer models
  413. foreach (LayerModel model in _layerModels)
  414. {
  415. model.Dispose();
  416. }
  417. _layerModels.Clear();
  418. // Clear groups
  419. _layerGroups.Clear();
  420. _layerToGroup.Clear();
  421. _tilemap = null;
  422. }
  423. /// <summary>
  424. /// Sets the default rendering mode for ungrouped layers.
  425. /// </summary>
  426. /// <param name="mode">The render mode.</param>
  427. /// <remarks>
  428. /// <para>
  429. /// This affects the behavior of <see cref="Draw(OrthographicCamera)"/> for layers that are not
  430. /// in any group.
  431. /// </para>
  432. /// <para>
  433. /// <see cref="RenderMode.Merged"/>: All ungrouped layers are merged into a single
  434. /// draw call (default).
  435. /// </para>
  436. /// <para>
  437. /// <see cref="RenderMode.Separate"/>: Each ungrouped layer is drawn individually.
  438. /// </para>
  439. /// </remarks>
  440. public void SetDefaultRenderMode(RenderMode mode)
  441. {
  442. ThrowIfDisposed();
  443. _defaultRenderMode = mode;
  444. }
  445. /// <summary>
  446. /// Defines a layer group for merged rendering.
  447. /// </summary>
  448. /// <param name="groupName">The name of the group.</param>
  449. /// <param name="layerNames">The names of the layers to include in the group.</param>
  450. /// <remarks>
  451. /// <para>
  452. /// Layers in a group are merged into a single draw call for optimal performance.
  453. /// Layers are rendered in the order specified, back to front.
  454. /// </para>
  455. /// <para>
  456. /// Groups can be redefined at any time. The merged vertex/index buffers are rebuilt
  457. /// when the group is next drawn.
  458. /// </para>
  459. /// <para>
  460. /// Each layer can only belong to one group. If a layer is already in another group,
  461. /// it will be removed from the previous group and added to the new one.
  462. /// </para>
  463. /// </remarks>
  464. /// <exception cref="ArgumentNullException">Thrown if groupName or layerNames is null.</exception>
  465. /// <exception cref="ArgumentException">Thrown if any layer name is not found in the tilemap.</exception>
  466. /// <exception cref="InvalidOperationException">Thrown if no tilemap is loaded.</exception>
  467. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  468. public void DefineLayerGroup(string groupName, params string[] layerNames)
  469. {
  470. ThrowIfDisposed();
  471. ThrowIfNoTilemap();
  472. if (groupName == null)
  473. throw new ArgumentNullException(nameof(groupName));
  474. if (layerNames == null)
  475. throw new ArgumentNullException(nameof(layerNames));
  476. // Will be fully implemented in Week 2
  477. // For now, just validate layer names exist
  478. List<TilemapLayer> layers = new List<TilemapLayer>();
  479. foreach (string layerName in layerNames)
  480. {
  481. TilemapLayer layer = _tilemap.Layers[layerName];
  482. if (layer == null)
  483. throw new ArgumentException($"Layer '{layerName}' not found in tilemap.", nameof(layerNames));
  484. layers.Add(layer);
  485. }
  486. }
  487. /// <summary>
  488. /// Defines a layer group using layer indices.
  489. /// </summary>
  490. /// <param name="groupName">The name of the group.</param>
  491. /// <param name="startIndex">The index of the first layer.</param>
  492. /// <param name="count">The number of layers to include.</param>
  493. /// <remarks>
  494. /// Each layer can only belong to one group. If a layer is already in another group,
  495. /// it will be removed from the previous group and added to the new one.
  496. /// </remarks>
  497. /// <exception cref="ArgumentNullException">Thrown if groupName is null.</exception>
  498. /// <exception cref="ArgumentOutOfRangeException">Thrown if layer indices are out of range.</exception>
  499. /// <exception cref="InvalidOperationException">Thrown if no tilemap is loaded.</exception>
  500. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  501. public void DefineLayerGroup(string groupName, int startIndex, int count)
  502. {
  503. ThrowIfDisposed();
  504. ThrowIfNoTilemap();
  505. if (groupName == null)
  506. throw new ArgumentNullException(nameof(groupName));
  507. if (startIndex < 0 || startIndex >= _tilemap.Layers.Count)
  508. throw new ArgumentOutOfRangeException(nameof(startIndex));
  509. if (count <= 0 || startIndex + count > _tilemap.Layers.Count)
  510. throw new ArgumentOutOfRangeException(nameof(count));
  511. // Will be fully implemented in Week 2
  512. }
  513. /// <summary>
  514. /// Removes a layer group.
  515. /// </summary>
  516. /// <param name="groupName">The name of the group to remove.</param>
  517. /// <remarks>
  518. /// Layers in the removed group become ungrouped and will be drawn individually
  519. /// if <see cref="Draw(OrthographicCamera)"/> is called.
  520. /// </remarks>
  521. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  522. public void RemoveLayerGroup(string groupName)
  523. {
  524. ThrowIfDisposed();
  525. if (_layerGroups.ContainsKey(groupName))
  526. {
  527. LayerGroup group = _layerGroups[groupName];
  528. // Remove layer→group mappings
  529. foreach (TilemapLayer layer in group.Layers)
  530. {
  531. _layerToGroup.Remove(layer);
  532. }
  533. // Dispose group's merged model
  534. group.MergedModel?.Dispose();
  535. _layerGroups.Remove(groupName);
  536. }
  537. }
  538. /// <summary>
  539. /// Gets whether a layer group is defined.
  540. /// </summary>
  541. /// <param name="groupName">The name of the group.</param>
  542. /// <returns>True if the group exists; otherwise, false.</returns>
  543. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  544. public bool HasLayerGroup(string groupName)
  545. {
  546. ThrowIfDisposed();
  547. return _layerGroups.ContainsKey(groupName);
  548. }
  549. /// <summary>
  550. /// Marks a layer group as needing rebuild.
  551. /// </summary>
  552. /// <param name="groupName">The name of the group.</param>
  553. /// <remarks>
  554. /// Call this after modifying tiles in layers that belong to a group.
  555. /// The group will be rebuilt on the next draw call.
  556. /// </remarks>
  557. /// <exception cref="ArgumentException">Thrown if the group does not exist.</exception>
  558. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  559. public void MarkGroupDirty(string groupName)
  560. {
  561. ThrowIfDisposed();
  562. if (!_layerGroups.TryGetValue(groupName, out LayerGroup group))
  563. throw new ArgumentException($"Layer group '{groupName}' does not exist.", nameof(groupName));
  564. group.IsDirty = true;
  565. }
  566. /// <summary>
  567. /// Rebuilds the merged vertex/index buffers for a layer group.
  568. /// </summary>
  569. /// <param name="groupName">The name of the group.</param>
  570. /// <remarks>
  571. /// This is called automatically when drawing a dirty group, but can be called
  572. /// manually to control when the rebuild happens (e.g., during loading).
  573. /// </remarks>
  574. /// <exception cref="ArgumentException">Thrown if the group does not exist.</exception>
  575. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  576. public void RebuildLayerGroup(string groupName)
  577. {
  578. ThrowIfDisposed();
  579. if (!_layerGroups.TryGetValue(groupName, out LayerGroup group))
  580. throw new ArgumentException($"Layer group '{groupName}' does not exist.", nameof(groupName));
  581. // Will be fully implemented in Week 2
  582. // RebuildLayerGroupInternal(group);
  583. }
  584. /// <summary>
  585. /// Begins a manual rendering sequence.
  586. /// </summary>
  587. /// <param name="camera">The camera for view and projection matrices.</param>
  588. /// <remarks>
  589. /// Call this before <see cref="DrawLayerGroup"/> or <see cref="DrawLayer(string)"/>.
  590. /// Must be paired with <see cref="EndDraw"/>.
  591. /// Automatically saves GraphicsDevice state for restoration after SpriteBatch usage.
  592. /// </remarks>
  593. /// <exception cref="ArgumentNullException">Thrown if camera is null.</exception>
  594. /// <exception cref="InvalidOperationException">Thrown if no tilemap is loaded or already drawing.</exception>
  595. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  596. public void BeginDraw(OrthographicCamera camera)
  597. {
  598. ThrowIfDisposed();
  599. ThrowIfNoTilemap();
  600. if (camera == null)
  601. throw new ArgumentNullException(nameof(camera));
  602. if (_isDrawing)
  603. throw new InvalidOperationException("BeginDraw already called. Call EndDraw first.");
  604. _isDrawing = true;
  605. // Save GraphicsDevice state for SpriteBatch mixing
  606. SaveGraphicsDeviceState();
  607. // Configure sampler state to prevent tile seams
  608. _graphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
  609. // Set up matrices
  610. _viewMatrix = camera.GetViewMatrix();
  611. _projectionMatrix = Matrix.CreateOrthographicOffCenter(
  612. 0, _graphicsDevice.Viewport.Width,
  613. _graphicsDevice.Viewport.Height, 0,
  614. 0, 1);
  615. _effect.View = _viewMatrix;
  616. _effect.Projection = _projectionMatrix;
  617. _effect.World = Matrix.Identity;
  618. }
  619. /// <summary>
  620. /// Draws a layer group.
  621. /// </summary>
  622. /// <param name="groupName">The name of the layer group.</param>
  623. /// <remarks>
  624. /// All layers in the group are rendered in a single draw call.
  625. /// Must be called between <see cref="BeginDraw"/> and <see cref="EndDraw"/>.
  626. /// If the group is marked dirty, it will be rebuilt before drawing.
  627. /// </remarks>
  628. /// <exception cref="InvalidOperationException">Thrown if called outside of BeginDraw/EndDraw.</exception>
  629. /// <exception cref="ArgumentException">Thrown if the group does not exist.</exception>
  630. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  631. public void DrawLayerGroup(string groupName)
  632. {
  633. ThrowIfDisposed();
  634. ThrowIfNotDrawing();
  635. if (!_layerGroups.TryGetValue(groupName, out LayerGroup group))
  636. throw new ArgumentException($"Layer group '{groupName}' does not exist.", nameof(groupName));
  637. // Will be fully implemented in Week 2
  638. // if (group.IsDirty)
  639. // RebuildLayerGroupInternal(group);
  640. //
  641. // DrawLayerModel(group.MergedModel);
  642. }
  643. /// <summary>
  644. /// Draws a layer model to the screen.
  645. /// </summary>
  646. /// <param name="model">The layer model to draw.</param>
  647. private void DrawLayerModel(LayerModel model)
  648. {
  649. if (model == null)
  650. return;
  651. // Set the texture
  652. _effect.Texture = model.Texture;
  653. // Bind vertex and index buffers
  654. _graphicsDevice.SetVertexBuffer(model.VertexBuffer);
  655. _graphicsDevice.Indices = model.IndexBuffer;
  656. // Apply the effect and draw
  657. foreach (EffectPass pass in _effect.CurrentTechnique.Passes)
  658. {
  659. pass.Apply();
  660. _graphicsDevice.DrawIndexedPrimitives(
  661. PrimitiveType.TriangleList,
  662. baseVertex: 0,
  663. startIndex: 0,
  664. primitiveCount: model.PrimitiveCount);
  665. }
  666. }
  667. /// <summary>
  668. /// Draws a single layer by name.
  669. /// </summary>
  670. /// <param name="layerName">The name of the layer.</param>
  671. /// <remarks>
  672. /// If the layer is part of a group, only this layer is drawn (not the entire group).
  673. /// Must be called between <see cref="BeginDraw"/> and <see cref="EndDraw"/>.
  674. /// </remarks>
  675. /// <exception cref="InvalidOperationException">Thrown if called outside of BeginDraw/EndDraw.</exception>
  676. /// <exception cref="ArgumentException">Thrown if the layer does not exist.</exception>
  677. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  678. public void DrawLayer(string layerName)
  679. {
  680. ThrowIfDisposed();
  681. ThrowIfNotDrawing();
  682. TilemapLayer layer = _tilemap.Layers[layerName];
  683. if (layer == null)
  684. throw new ArgumentException($"Layer '{layerName}' not found in tilemap.", nameof(layerName));
  685. DrawLayerInternal(layer);
  686. }
  687. /// <summary>
  688. /// Draws a single layer by index.
  689. /// </summary>
  690. /// <param name="layerIndex">The index of the layer.</param>
  691. /// <remarks>
  692. /// If the layer is part of a group, only this layer is drawn (not the entire group).
  693. /// Must be called between <see cref="BeginDraw"/> and <see cref="EndDraw"/>.
  694. /// </remarks>
  695. /// <exception cref="InvalidOperationException">Thrown if called outside of BeginDraw/EndDraw.</exception>
  696. /// <exception cref="ArgumentOutOfRangeException">Thrown if the layer index is out of range.</exception>
  697. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  698. public void DrawLayer(int layerIndex)
  699. {
  700. ThrowIfDisposed();
  701. ThrowIfNotDrawing();
  702. if (layerIndex < 0 || layerIndex >= _tilemap.Layers.Count)
  703. throw new ArgumentOutOfRangeException(nameof(layerIndex));
  704. DrawLayerInternal(_tilemap.Layers[layerIndex]);
  705. }
  706. /// <summary>
  707. /// Internal method to draw a single layer.
  708. /// </summary>
  709. /// <param name="layer">The layer to draw.</param>
  710. private void DrawLayerInternal(TilemapLayer layer)
  711. {
  712. // Skip invisible layers
  713. if (!layer.IsVisible)
  714. return;
  715. // Only tile layers are supported in Week 1
  716. // Object/Image/Group layers will be added in Week 4
  717. if (layer is not TilemapTileLayer)
  718. return;
  719. // Find the corresponding layer model
  720. // For now, we match by layer index since models are built in order
  721. int layerIndex = _tilemap.Layers.IndexOf(layer);
  722. if (layerIndex < 0 || layerIndex >= _layerModels.Count)
  723. return;
  724. LayerModel model = _layerModels[layerIndex];
  725. // Apply layer opacity by modifying effect alpha
  726. // (This is a simplified approach; full implementation in Week 4)
  727. _effect.Alpha = layer.Opacity;
  728. // Draw the layer model
  729. DrawLayerModel(model);
  730. // Restore alpha
  731. _effect.Alpha = 1.0f;
  732. }
  733. /// <summary>
  734. /// Ends a manual rendering sequence.
  735. /// </summary>
  736. /// <exception cref="InvalidOperationException">Thrown if BeginDraw was not called.</exception>
  737. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  738. public void EndDraw()
  739. {
  740. ThrowIfDisposed();
  741. ThrowIfNotDrawing();
  742. _isDrawing = false;
  743. }
  744. /// <summary>
  745. /// Draws all layers automatically.
  746. /// </summary>
  747. /// <param name="camera">The camera for view and projection matrices.</param>
  748. /// <remarks>
  749. /// <para>
  750. /// Draws all visible layers. Grouped layers are drawn as a group (single draw call).
  751. /// Ungrouped layers are drawn according to <see cref="DefaultRenderMode"/>.
  752. /// </para>
  753. /// <para>
  754. /// For fine-grained control over layer rendering order (e.g., to inject sprites
  755. /// between layers), use manual rendering with <see cref="BeginDraw(OrthographicCamera)"/>,
  756. /// <see cref="DrawLayerGroup"/>, and <see cref="EndDraw"/>.
  757. /// </para>
  758. /// <para>
  759. /// <strong>Note:</strong> This method is for single tilemap mode only.
  760. /// For world mode (LDtk GridVania), use <see cref="DrawWorld(OrthographicCamera, int)"/>
  761. /// to specify which WorldDepth to render.
  762. /// </para>
  763. /// </remarks>
  764. /// <exception cref="ArgumentNullException">Thrown if camera is null.</exception>
  765. /// <exception cref="InvalidOperationException">Thrown if no tilemap is loaded or if in world mode.</exception>
  766. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  767. public void Draw(OrthographicCamera camera)
  768. {
  769. ThrowIfDisposed();
  770. ThrowIfNoTilemap();
  771. if (camera == null)
  772. throw new ArgumentNullException(nameof(camera));
  773. if (_isWorldMode)
  774. throw new InvalidOperationException("Cannot use Draw() in world mode. Use DrawWorld(camera, worldDepth) instead.");
  775. // Single tilemap mode
  776. BeginDraw(camera);
  777. // Week 1: Simple implementation - draw all layers in order
  778. // Week 2: Will add layer grouping support
  779. // Week 3: Will add RenderMode support for ungrouped layers
  780. for (int i = 0; i < _tilemap.Layers.Count; i++)
  781. {
  782. DrawLayer(i);
  783. }
  784. EndDraw();
  785. }
  786. /// <summary>
  787. /// Draws all tilemaps at the specified WorldDepth.
  788. /// </summary>
  789. /// <param name="camera">The camera for view and projection matrices.</param>
  790. /// <param name="worldDepth">The WorldDepth to render. Only tilemaps with this exact depth will be drawn.</param>
  791. /// <remarks>
  792. /// <para>
  793. /// This method is designed for LDtk GridVania worlds where levels are organized by WorldDepth.
  794. /// The game typically renders one depth at a time based on gameplay context:
  795. /// - WorldDepth 0: Main overworld
  796. /// - WorldDepth 1: Building interiors, second floors
  797. /// - WorldDepth -1: Underground, basements
  798. /// </para>
  799. /// <para>
  800. /// This matches how the LDtk editor displays levels - showing one depth layer at a time.
  801. /// </para>
  802. /// </remarks>
  803. /// <exception cref="ArgumentNullException">Thrown if camera is null.</exception>
  804. /// <exception cref="InvalidOperationException">Thrown if not in world mode.</exception>
  805. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  806. public void DrawWorld(OrthographicCamera camera, int worldDepth)
  807. {
  808. ThrowIfDisposed();
  809. if (camera == null)
  810. throw new ArgumentNullException(nameof(camera));
  811. if (!_isWorldMode)
  812. throw new InvalidOperationException("Not in world mode. Use LoadWorld() first, or use Draw() for single tilemap.");
  813. // Configure sampler state to prevent tile seams
  814. _graphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
  815. // Set up matrices
  816. _viewMatrix = camera.GetViewMatrix();
  817. _projectionMatrix = Matrix.CreateOrthographicOffCenter(
  818. 0, _graphicsDevice.Viewport.Width,
  819. _graphicsDevice.Viewport.Height, 0,
  820. 0, 1);
  821. _effect.View = _viewMatrix;
  822. _effect.Projection = _projectionMatrix;
  823. _effect.World = Matrix.Identity;
  824. // Draw only tilemaps at the specified WorldDepth
  825. foreach (WorldTilemap worldTilemap in _worldTilemaps)
  826. {
  827. if (worldTilemap.WorldDepth != worldDepth)
  828. continue; // Skip levels not at this depth
  829. // Draw all layer models for this tilemap
  830. foreach (LayerModel model in worldTilemap.LayerModels)
  831. {
  832. DrawLayerModel(model);
  833. }
  834. }
  835. }
  836. /// <summary>
  837. /// Updates animated tiles.
  838. /// </summary>
  839. /// <param name="gameTime">The game time.</param>
  840. /// <remarks>
  841. /// Must be called each frame to update tile animations.
  842. /// Automatically marks groups containing animated tiles as dirty when frames change.
  843. /// </remarks>
  844. /// <exception cref="ArgumentNullException">Thrown if gameTime is null.</exception>
  845. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  846. public void Update(GameTime gameTime)
  847. {
  848. ThrowIfDisposed();
  849. if (gameTime == null)
  850. throw new ArgumentNullException(nameof(gameTime));
  851. // Will be fully implemented in Week 4
  852. // UpdateAnimations(gameTime);
  853. }
  854. /// <summary>
  855. /// Saves the current GraphicsDevice state.
  856. /// </summary>
  857. /// <remarks>
  858. /// Call this before using SpriteBatch between <see cref="BeginDraw"/> and <see cref="EndDraw"/>.
  859. /// Paired with <see cref="RestoreGraphicsDeviceState"/>.
  860. /// Note: <see cref="BeginDraw"/> automatically saves state, so this is only needed for
  861. /// additional state changes.
  862. /// </remarks>
  863. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  864. public void SaveGraphicsDeviceState()
  865. {
  866. ThrowIfDisposed();
  867. _savedBlendState = _graphicsDevice.BlendState;
  868. _savedSamplerState = _graphicsDevice.SamplerStates[0];
  869. _savedRasterizerState = _graphicsDevice.RasterizerState;
  870. _savedDepthStencilState = _graphicsDevice.DepthStencilState;
  871. }
  872. /// <summary>
  873. /// Restores previously saved GraphicsDevice state.
  874. /// </summary>
  875. /// <remarks>
  876. /// Call this after using SpriteBatch between <see cref="BeginDraw"/> and <see cref="EndDraw"/>.
  877. /// Paired with <see cref="SaveGraphicsDeviceState"/>.
  878. /// </remarks>
  879. /// <exception cref="ObjectDisposedException">Thrown if this renderer has been disposed.</exception>
  880. public void RestoreGraphicsDeviceState()
  881. {
  882. ThrowIfDisposed();
  883. _graphicsDevice.BlendState = _savedBlendState;
  884. _graphicsDevice.SamplerStates[0] = _savedSamplerState;
  885. _graphicsDevice.RasterizerState = _savedRasterizerState;
  886. _graphicsDevice.DepthStencilState = _savedDepthStencilState;
  887. }
  888. /// <summary>
  889. /// Disposes all GPU resources used by this renderer.
  890. /// </summary>
  891. public void Dispose()
  892. {
  893. if (_isDisposed)
  894. return;
  895. UnloadTilemap();
  896. _effect?.Dispose();
  897. _effect = null;
  898. _isDisposed = true;
  899. }
  900. private void ThrowIfDisposed()
  901. {
  902. if (_isDisposed)
  903. throw new ObjectDisposedException(nameof(TilemapRenderer));
  904. }
  905. private void ThrowIfNoTilemap()
  906. {
  907. if (_tilemap == null && !_isWorldMode)
  908. throw new InvalidOperationException("No tilemap loaded. Call LoadTilemap or LoadWorld first.");
  909. }
  910. private void ThrowIfNotDrawing()
  911. {
  912. if (!_isDrawing)
  913. throw new InvalidOperationException("Not in drawing state. Call BeginDraw first.");
  914. }
  915. // Internal classes for layer management
  916. private sealed class LayerGroup
  917. {
  918. public List<TilemapLayer> Layers { get; set; }
  919. public LayerModel MergedModel { get; set; }
  920. public bool IsDirty { get; set; }
  921. }
  922. private sealed class LayerModel : IDisposable
  923. {
  924. public VertexBuffer VertexBuffer { get; set; }
  925. public IndexBuffer IndexBuffer { get; set; }
  926. public Texture2D Texture { get; set; }
  927. public int PrimitiveCount { get; set; }
  928. public void Dispose()
  929. {
  930. VertexBuffer?.Dispose();
  931. IndexBuffer?.Dispose();
  932. }
  933. }
  934. private sealed class WorldTilemap
  935. {
  936. public Tilemap Tilemap { get; set; }
  937. public Vector2 WorldPosition { get; set; }
  938. public int WorldDepth { get; set; }
  939. public List<LayerModel> LayerModels { get; set; }
  940. public WorldTilemap()
  941. {
  942. LayerModels = new List<LayerModel>();
  943. }
  944. }
  945. }