(原) 城市建造者,isocity开源小城市建造

原创文章,请后转载,并注明出处。

github.com/victorqribeiro/isocity

一个简单的开源项目,让你手动建造小城市。

另外,我还在这里看到一个类似的。更复杂一些,可以进行地图浏览(移动)

它只有四个文件,css、js、图片、html,而且代码也不多,值得啃一啃,用在自己的项目中。

有一段时间很想做一个以本地地图为背景的多人智力游戏,但终因缺少美工等问题而搁置,将以上思路结合应用,或许可以做一个实时地图生成(先用此项目生成全景地图,然后转换为代码保存)

运行时,你可以看到地址栏的变化类似

http://127.0.0.1:8080/#Li4ALg9DAAAuAC4DAAEpAhcCCQICAy4ALg86OgMuAC4DADoDLi4uDzo6CAICAgklOg==

其中#号后是经过Base64编码的地图信息

在CSS文件中,#tools > div 部份定义了背景图片,及每个小图大小。

以下代码已有修改

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta name="theme-color" content="#FFF"/>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<meta name="mobile-web-app-capable" content="yes">
	<meta property="og:image:width" content="512" />
	<meta property="og:image:height" content="512" />
	<link rel="icon" href="favicon.png" sizes="256x256"/>
	<link rel="apple-touch-icon" href="favicon.png" />
	<title> IsoCity </title>
	<link rel="stylesheet" type="text/css" href="main.css" />
</head>
<body>

<section id="main">
	<div id="tools"></div>
	<div id="area">
		<canvas id="bg"></canvas>
		<canvas id="fg"></canvas>
	</div>
</section>
<script src="main.js"></script>

</body>
</html>


const $ = _ => document.querySelector(_)

const $c = _ => document.createElement(_)

let canvas, bg, fg, cf, ntiles, tileWidth, tileHeight, map, tools, tool, activeTool, isPlacing

/* texture from https://opengameart.org/content/isometric-landscape */
const texture = new Image()
texture.src = "textures/01_130x66_130x230.png"
texture.onload = _ => init()

const init = function(){

	tool = [0,0]

	map = [
		[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
		[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
		[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
		[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
		[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
		[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
		[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]]
	]

	canvas = $("#bg")
	canvas.width = 910
	canvas.height = 666
	w = 910
	h = 462
	texWidth = 12
	texHeight = 6
	bg = canvas.getContext("2d")
	ntiles = 7
	tileWidth = 128
	tileHeight = 64
	bg.translate(w/2,tileHeight*2)

	loadHashState(document.location.hash.substring(1))

	drawMap()

	fg = $('#fg')
	fg.width = canvas.width
	fg.height = canvas.height
	cf = fg.getContext('2d')
	cf.translate(w/2,tileHeight*2)
	fg.addEventListener('mousemove', viz)
	fg.addEventListener('contextmenu', e => { e.preventDefault() } )
	fg.addEventListener('mouseup', unclick)
	fg.addEventListener('mousedown', click)
	fg.addEventListener('touchend', click)
	fg.addEventListener('pointerup', click)

	tools = $('#tools')

	let toolCount = 0
	for(let i = 0; i < texHeight; i++){
		for(let j = 0; j < texWidth; j++){
			const div = $c('div');
			div.id = `tool_${toolCount++}`
			div.style.display = "block"
			/* width of 132 instead of 130  = 130 image + 2 border = 132 */
			div.style.backgroundPosition = `-${j*130+2}px -${i*230}px`
			div.addEventListener('click', e => {
				tool = [i,j]
				if (activeTool)
					$(`#${activeTool}`).classList.remove('selected')
				activeTool = e.target.id
				$(`#${activeTool}`).classList.add('selected')
			})
			tools.appendChild( div )
		}
	}

}

// From https://stackoverflow.com/a/36046727
const ToBase64 = function(u8){
	return btoa(String.fromCharCode.apply(null, u8))
}

const FromBase64 = function(str){
	return atob(str).split('').map( c => c.charCodeAt(0) )
}

function updateHashState() {
	let c = 0
	const u8 = new Uint8Array(ntiles*ntiles)
	for(let i = 0; i < ntiles; i++){
		for(let j = 0; j < ntiles; j++){
			u8[c++] = map[i][j][0]*texWidth + map[i][j][1]
		}
	}
	const state = ToBase64(u8)
	history.replaceState(undefined, undefined, `#${state}`)
}

function loadHashState(state) {
	let u8 = FromBase64(state)
	let c = 0
	for(let i = 0; i < ntiles; i++) {
		for(let j = 0; j < ntiles; j++) {
			const t = u8[c++] || 0
			const x = Math.trunc(t / texWidth)
			const y = Math.trunc(t % texWidth)
			map[i][j] = [x,y]
		}
	}
}

const click = e => {
	const pos = getPosition(e)
	if (pos.x >= 0 && pos.x < ntiles && pos.y >= 0 && pos.y < ntiles) {
		
		map[pos.x][pos.y][0] = (e.which === 3) ? 0 : tool[0]
		map[pos.x][pos.y][1] = (e.which === 3) ? 0 : tool[1]
		isPlacing = true

		drawMap()
		cf.clearRect(-w, -h, w * 2, h * 2)
	}
	updateHashState();
}

const unclick = e => {
	if (isPlacing)
		isPlacing = false
}

const drawMap = function(){
	bg.clearRect(-w,-h,w*2,h*2)
	for(let i = 0; i < ntiles; i++){
		for(let j = 0; j < ntiles; j++){
			drawImageTile(bg,i,j,map[i][j][0],map[i][j][1])
		}
	}
}

const drawTile = function(c,x,y,color){
	c.save()
	c.translate((y-x) * tileWidth/2,(x+y)*tileHeight/2)
	c.beginPath()
	c.moveTo(0,0)
	c.lineTo(tileWidth/2,tileHeight/2)
	c.lineTo(0,tileHeight)
	c.lineTo(-tileWidth/2,tileHeight/2)
	c.closePath()
	c.fillStyle = color
	c.fill()
	c.restore()
}

const drawImageTile = function(c,x,y,i,j){
	c.save()
	c.translate((y-x) * tileWidth/2,(x+y)*tileHeight/2)
	j *= 130
	i *= 230
	c.drawImage(texture,j,i,130,230,-65,-130,130,230)
	c.restore()
}

const getPosition = e => {
	const _y =  (e.offsetY - tileHeight * 2) / tileHeight,
				_x =  e.offsetX / tileWidth - ntiles / 2
	x = Math.floor(_y-_x)
	y = Math.floor(_x+_y)
	return {x,y}
}

const viz = function(e){
	if (isPlacing)
		click(e)
	const pos = getPosition(e)
	cf.clearRect(-w,-h,w*2,h*2)
	if( pos.x >= 0 && pos.x < ntiles && pos.y >= 0 && pos.y < ntiles)
		drawTile(cf,pos.x,pos.y,'rgba(0,0,0,0.2)')
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}

canvas {
  display: block;
  position: absolute;
}

#main {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: space-around;
	height: 100%;
}

#area {
	position: relative;
	width: 100%;
	height: 100%;
	flex: 1;
	display: flex;
	justify-content: space-around;
	overflow: auto;
}

#tools {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	flex-wrap: wrap;
	overflow: auto;
	width: 580px;
	height: 100%;
	transition: width .8s;
}
@media only screen and (max-width: 1700px) {
	#tools {
		width: 440px;
	}
}

@media only screen and (max-width: 1540px) {
	#tools {
		width: 300px;
	}
}
@media only screen and (max-width: 1380px) {
	#tools {
		width: 160px;
	}
}

#tools > div {
	display: block;
	background-image: url('../textures/01_130x66_130x230.png');
	background-repeat: no-repeat;
	background-size: auto;
	width: 130px;
	height: 230px;
	border: 2px dashed transparent;
	box-sizing: border-box;
}

#tools > div.selected {
	border-color: #b05355;
}

@media only screen and (max-width: 966px) {

	#main {
		position: relative;
		display: flex;
		flex-direction: column;
		align-items: flex-start;
	}
	
	#tools {
		display: flex;
		align-items: center;
		overflow: auto;
		width: 100%;
		height: 240px;
	}
	#area {
		justify-content: flex-start;
	}
	
}