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.