How I Built a City‑Building Game with Three.js, Vue, and Pinia
After a two‑month hiatus, the author details the creation of CubeCity—a 17×17 grid city‑building game built with Three.js, Vue, and Pinia—covering metadata design, rendering pipeline, interaction modes, code architecture, and reflections on using Three.js for game development.
Overview
The author returns after two months and introduces CubeCity , a city‑building game built with Three.js, Vue, and Pinia.
Page preview
Screenshot of the game UI and basic gameplay.
Core features
Four interaction modes: Build, Select, Relocate, Demolish.
Metadata‑driven architecture: a 17×17 grid stored in Pinia, each tile holds type, building, direction, level, and detail.
Conversion from metadata to a 3D scene via Tile, City, and World classes. SimObject base class for all interactive objects, handling mesh cloning, highlighting, and animation.
Raycaster limited to the tile group for efficient picking.
Metadata design
Each tile is an object with fields such as:
{
"type": "grass",
"building": null,
"direction": 0,
"level": 1,
"detail": {
"coinOutput": 70,
"powerUsage": 40,
"pollution": 22,
"population": 20,
"category": "industrial"
},
"outputFactor": 1
}The Pinia store initializes a 17×17 array:
export const useGameState = defineStore('gameState', {
state: () => ({
metadata: Array.from({ length: 17 }, _ =>
Array.from({ length: 17 }, _ => ({
type: 'grass',
building: null,
direction: 0
}))
),
currentMode: 'build',
selectedBuilding: null,
// … other state fields
})
});From metadata to Three.js
Worldcreates a City, which iterates the metadata, creates Tile instances and adds them to a single THREE.Group. Tile loads grass and ground meshes and, if a building exists, creates the building via a factory map.
const BUILDING_CLASS_MAP = {
house: House,
factory: Factory,
// other building classes
};
export function createBuilding(type, level = 1, direction = 0, options = {}) {
const Cls = BUILDING_CLASS_MAP[type];
return Cls ? new Cls(type, level, direction, options) : null;
}Interaction system
Mode‑driven logic is handled by an Interactor that reads currentMode from Pinia. In Build mode it checks canPlaceBuilding (requires adjacency to a road unless the building is in FREE_BUILDING_TYPES) and updates both the 3D scene and the metadata.
export function canPlaceBuilding(x, y, buildingType, metadata) {
if (FREE_BUILDING_TYPES.includes(buildingType)) return true;
const dirs = [[0,1],[1,0],[0,-1],[-1,0]];
return dirs.some(([dx,dy]) => {
const nx = x + dx, ny = y + dy;
return metadata[nx]?.[ny]?.type === 'ground' && metadata[nx][ny].building === 'road';
});
}Demolish mode emits a confirmation event, then removes the building from the tile and refunds part of the cost.
SimObject
All interactive objects extend SimObject, which clones GLTF meshes, assigns userData, and provides methods to set emission color and opacity for highlighting. Focused objects animate with a subtle Y‑axis bounce using GSAP.
Conclusion
The project demonstrates a metadata‑first approach that keeps game logic separate from rendering, making the Three.js scene a pure visual projection. It also highlights the limits of using Three.js as a full‑featured game engine and suggests moving to dedicated engines for larger projects.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
