Randomly Generated 2D Terrain/Maps

2023-07-03

First steps here, extremely basic - generating grassy fields with interspersed decorations, e.g. flowers, patches, etc., to avoid having to manually try to get a “natural” look. Sprites credit to Akizakura16 from this thread - only using them while testing, I’ll make (or probably generate) some original ones in the future. The example below isn’t a .gif/video, it’s actually being generated on this page using a single spritesheet + HTML5 <canvas>. RPG Maker MV appears to use Javascript for scripting/plugins, so hopefully not too much of a stretch to port over.

  1	<div style="display: flex; justify-content: center;">
  2		<canvas id="canvas" width="32" height="32"></canvas>
  3	</div>
  4	<script>
  5		fetch('/spritesheet_tiles.png').then(response => response.blob()).then(blob => {
  6			const img = new Image();
  7			const reader = new FileReader();
  8		
  9			reader.onloadend = function() {
 10				img.src = reader.result;
 11			
 12				img.onload = function() {
 13					// Define the size of each sprite frame
 14					const frameWidth = 32;
 15					const frameHeight = 32;
 16			
 17					// Calculate the number of frames in the sprite sheet
 18					let lenX = img.width / frameWidth;
 19					let lenY = img.height / frameHeight;
 20			
 21					// Loop through each frame in the sprite sheet
 22					// Builds an array of canvas "tiles" for later use
 23					let index = 0;
 24					const cells = [];
 25					for (let y = 0; y < lenY; y++) {
 26						for (let x = 0; x < lenX; x++) {
 27							const cellCanvas = document.createElement('canvas');
 28							cellCanvas.width = frameWidth;
 29							cellCanvas.height = frameHeight;
 30							const cellContext = cellCanvas.getContext('2d');
 31							cellContext.drawImage(
 32								img,
 33								x * frameWidth,
 34								y * frameHeight,
 35								frameWidth,
 36								frameHeight,
 37								0,
 38								0,
 39								frameWidth,
 40								frameHeight
 41							);
 42
 43							index++;
 44
 45							cells.push(cellCanvas);
 46						}
 47					}
 48
 49					// Grab the output canvas element
 50					const canvas = document.getElementById('canvas');
 51					const context = canvas.getContext('2d');
 52			
 53					// Set the canvas size
 54					canvas.width = canvas.parentNode.offsetWidth / 2;
 55					canvas.height = canvas.parentNode.offsetWidth / 2;
 56
 57					// Tweak for mobile, small screens
 58					if (canvas.parentNode.offsetWidth < 800) {
 59						canvas.width = canvas.parentNode.offsetWidth - 10;
 60						canvas.height = canvas.parentNode.offsetWidth - 10;
 61					}
 62
 63					// Max dimensions
 64					lenX = Math.ceil(canvas.width / frameWidth);
 65					lenY = Math.ceil(canvas.height / frameHeight);
 66
 67					// Grid, 2D array for manipulation since it makes more sense to me this way
 68					let grid = [];
 69					index = 0;
 70					for (let y = 0; y < lenY; y++) {
 71						let row = [];
 72						for (let x = 0; x < lenX; x++) {
 73							const cellX = x * frameWidth;
 74							const cellY = y * frameHeight;
 75							/** Each cell retains data about:
 76								* sequential position
 77								* canvas coordinates
 78								* grid array coordinates
 79								* tile sprite (default 20 = empty grass)
 80							**/
 81							row.push({
 82								"index": index,
 83								"x": cellX,
 84								"y": cellY,
 85								"coordx": x,
 86								"coordy": y,
 87								"tile": 20
 88							});
 89							index++;
 90						}
 91						grid.push(row);
 92					}
 93
 94					// Initialize the canvas with a base grass sprite
 95					for (let x = 0; x < lenX; x++) {
 96						for (let y = 0; y < lenY; y++) {
 97							const cell = grid[y][x];
 98							context.drawImage(cells[cell["tile"]], cell["x"], cell["y"]);
 99						}
100					}
101
102					// List of sprites from the spritesheet that are valid decoration for grass
103					const grassDeco = [19, 27, 28, 35, 36, 43, 44];
104					// Number of tiles to target for decoration, 30%
105					const percentDeco = 0.3;
106
107					function resetGrid(grid, base_tile) {
108						for (let x = 0; x < lenX; x++) {
109							for (let y = 0; y < lenY; y++) {
110								grid[y][x]["tile"] = base_tile;
111							}
112						}
113					}
114					
115					function repaintGrid(context, grid) {
116						for (let x = 0; x < lenX; x++) {
117							for (let y = 0; y < lenY; y++) {
118								const cell = grid[y][x];
119								context.drawImage(cells[cell["tile"]], cell["x"], cell["y"]);
120							}
121						}
122					}
123					
124					function paintDeco(context, grid, deco, percent) {
125						let grid_flat = grid.flat();
126						const maxCells = grid_flat.length;
127
128						for (let i = 0; i < Math.floor(percent * maxCells); i++) {
129							// Choose deco tile
130							const decoTile = deco[Math.floor(Math.random() * deco.length)];
131							// Pick a random cell from the flat array
132							const random_tile = Math.floor(Math.random() * grid_flat.length);
133							const cell = grid_flat[random_tile];
134							// Pop the chosen cell from the flat array
135							grid_flat = grid_flat.filter(tile => tile !== cell);
136							// Set the deco tile in the real grid array
137							grid[cell["coordy"]][cell["coordx"]]["tile"] = decoTile;
138
139							// This could totally be more efficient, but it is just a WIP for now.
140							// I expect the data structures in RMMV to be different anyways.
141						}
142					}
143
144					paintDeco(context, grid, grassDeco, percentDeco);
145					repaintGrid(context, grid);
146
147					setInterval(() => {
148						resetGrid(grid, 20);
149						paintDeco(context, grid, grassDeco, percentDeco);
150						repaintGrid(context, grid);
151					}, 2000);
152				};
153			};
154
155			reader.readAsDataURL(blob);
156		});
157	</script>

Next step, trees and other structures, followed by paths (e.g. through a forest), then walkways connecting buildings using doors as their endpoints.