标签:
译文:http://blog.jobbole.com/70956/
原文:http://www.playfuljs.com/a-first-person-engine-in-265-lines/
这是一篇关于利用 Canvas 实现3D 游戏场景绘制的文章,看完感觉很受启发,所以自己准备总结一下。我们先看下最终效果:
很酷是不是。原作者只使用了265行代码就实现了这么炫酷的效果。代码写的也很清晰明了,并且原文也对原理也进行了解释。本篇就直接将原作者的代码全部贴上来大家观赏观赏。
1 <!doctype html> 2 <html> 3 <head> 4 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 5 <title>Raycaster Demo - PlayfulJS</title> 6 </head> 7 <body style=‘background: #000; margin: 0; padding: 0; width: 100%; height: 100%;‘> 8 <canvas id=‘display‘ width=‘1‘ height=‘1‘ style=‘width: 100%; height: 100%;‘ /> 9 10 <script> 11 12 var CIRCLE = Math.PI * 2; 13 var MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) 14 15 function Controls() { 16 this.codes = { 37: ‘left‘, 39: ‘right‘, 38: ‘forward‘, 40: ‘backward‘ }; 17 this.states = { ‘left‘: false, ‘right‘: false, ‘forward‘: false, ‘backward‘: false }; 18 document.addEventListener(‘keydown‘, this.onKey.bind(this, true), false); 19 document.addEventListener(‘keyup‘, this.onKey.bind(this, false), false); 20 document.addEventListener(‘touchstart‘, this.onTouch.bind(this), false); 21 document.addEventListener(‘touchmove‘, this.onTouch.bind(this), false); 22 document.addEventListener(‘touchend‘, this.onTouchEnd.bind(this), false); 23 } 24 25 Controls.prototype.onTouch = function(e) { 26 var t = e.touches[0]; 27 this.onTouchEnd(e); 28 if (t.pageY < window.innerHeight * 0.5) this.onKey(true, { keyCode: 38 }); 29 else if (t.pageX < window.innerWidth * 0.5) this.onKey(true, { keyCode: 37 }); 30 else if (t.pageY > window.innerWidth * 0.5) this.onKey(true, { keyCode: 39 }); 31 }; 32 33 Controls.prototype.onTouchEnd = function(e) { 34 this.states = { ‘left‘: false, ‘right‘: false, ‘forward‘: false, ‘backward‘: false }; 35 e.preventDefault(); 36 e.stopPropagation(); 37 }; 38 39 Controls.prototype.onKey = function(val, e) { 40 var state = this.codes[e.keyCode]; 41 if (typeof state === ‘undefined‘) return; 42 this.states[state] = val; 43 e.preventDefault && e.preventDefault(); 44 e.stopPropagation && e.stopPropagation(); 45 console.log(e.keyCode); 46 }; 47 48 function Bitmap(src, width, height) { 49 this.image = new Image(); 50 this.image.src = src; 51 this.width = width; 52 this.height = height; 53 } 54 55 function Player(x, y, direction) { 56 this.x = x; 57 this.y = y; 58 this.direction = direction; 59 this.weapon = new Bitmap(‘assets/knife_hand.png‘, 319, 320); 60 this.paces = 0; 61 } 62 63 Player.prototype.rotate = function(angle) { 64 this.direction = (this.direction + angle + CIRCLE) % (CIRCLE); 65 }; 66 67 Player.prototype.walk = function(distance, map) { 68 var dx = Math.cos(this.direction) * distance; 69 var dy = Math.sin(this.direction) * distance; 70 if (map.get(this.x + dx, this.y) <= 0) this.x += dx; 71 if (map.get(this.x, this.y + dy) <= 0) this.y += dy; 72 this.paces += distance; 73 }; 74 75 Player.prototype.update = function(controls, map, seconds) { 76 if (controls.left) this.rotate(-Math.PI * seconds); 77 if (controls.right) this.rotate(Math.PI * seconds); 78 if (controls.forward) this.walk(3 * seconds, map); 79 if (controls.backward) this.walk(-3 * seconds, map); 80 }; 81 82 function Map(size) { 83 this.size = size; 84 this.wallGrid = new Uint8Array(size * size); 85 this.skybox = new Bitmap(‘assets/deathvalley_panorama.jpg‘, 2000, 750); 86 this.wallTexture = new Bitmap(‘assets/wall_texture.jpg‘, 1024, 1024); 87 this.light = 0; 88 } 89 90 Map.prototype.get = function(x, y) { 91 x = Math.floor(x); 92 y = Math.floor(y); 93 if (x < 0 || x > this.size - 1 || y < 0 || y > this.size - 1) return -1; 94 return this.wallGrid[y * this.size + x]; 95 }; 96 97 Map.prototype.randomize = function() { 98 for (var i = 0; i < this.size * this.size; i++) { 99 this.wallGrid[i] = Math.random() < 0.3 ? 1 : 0; 100 } 101 }; 102 103 Map.prototype.cast = function(point, angle, range) { 104 var self = this; 105 var sin = Math.sin(angle); 106 var cos = Math.cos(angle); 107 var noWall = { length2: Infinity }; 108 109 return ray({ x: point.x, y: point.y, height: 0, distance: 0 }); 110 111 function ray(origin) { 112 var stepX = step(sin, cos, origin.x, origin.y); 113 var stepY = step(cos, sin, origin.y, origin.x, true); 114 var nextStep = stepX.length2 < stepY.length2 115 ? inspect(stepX, 1, 0, origin.distance, stepX.y) 116 : inspect(stepY, 0, 1, origin.distance, stepY.x); 117 118 if (nextStep.distance > range) return [origin]; 119 return [origin].concat(ray(nextStep)); 120 } 121 122 function step(rise, run, x, y, inverted) { 123 if (run === 0) return noWall; 124 var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x; 125 var dy = dx * (rise / run); 126 return { 127 x: inverted ? y + dy : x + dx, 128 y: inverted ? x + dx : y + dy, 129 length2: dx * dx + dy * dy 130 }; 131 } 132 133 function inspect(step, shiftX, shiftY, distance, offset) { 134 var dx = cos < 0 ? shiftX : 0; 135 var dy = sin < 0 ? shiftY : 0; 136 step.height = self.get(step.x - dx, step.y - dy); 137 step.distance = distance + Math.sqrt(step.length2); 138 if (shiftX) step.shading = cos < 0 ? 2 : 0; 139 else step.shading = sin < 0 ? 2 : 1; 140 step.offset = offset - Math.floor(offset); 141 return step; 142 } 143 }; 144 145 Map.prototype.update = function(seconds) { 146 if (this.light > 0) this.light = Math.max(this.light - 10 * seconds, 0); 147 else if (Math.random() * 5 < seconds) this.light = 2; 148 }; 149 150 function Camera(canvas, resolution, focalLength) { 151 this.ctx = canvas.getContext(‘2d‘); 152 this.width = canvas.width = window.innerWidth * 0.5; 153 this.height = canvas.height = window.innerHeight * 0.5; 154 this.resolution = resolution; 155 this.spacing = this.width / resolution; 156 this.focalLength = focalLength || 0.8; 157 this.range = MOBILE ? 8 : 14; 158 this.lightRange = 5; 159 this.scale = (this.width + this.height) / 1200; 160 } 161 162 Camera.prototype.render = function(player, map) { 163 this.drawSky(player.direction, map.skybox, map.light); 164 this.drawColumns(player, map); 165 this.drawWeapon(player.weapon, player.paces); 166 }; 167 168 Camera.prototype.drawSky = function(direction, sky, ambient) { 169 var width = sky.width * (this.height / sky.height) * 2; 170 var left = (direction / CIRCLE) * -width; 171 172 this.ctx.save(); 173 this.ctx.drawImage(sky.image, left, 0, width, this.height); 174 if (left < width - this.width) { 175 this.ctx.drawImage(sky.image, left + width, 0, width, this.height); 176 } 177 if (ambient > 0) { 178 this.ctx.fillStyle = ‘#ffffff‘; 179 this.ctx.globalAlpha = ambient * 0.1; 180 this.ctx.fillRect(0, this.height * 0.5, this.width, this.height * 0.5); 181 } 182 this.ctx.restore(); 183 }; 184 185 Camera.prototype.drawColumns = function(player, map) { 186 this.ctx.save(); 187 for (var column = 0; column < this.resolution; column++) { 188 var x = column / this.resolution - 0.5; 189 var angle = Math.atan2(x, this.focalLength); 190 var ray = map.cast(player, player.direction + angle, this.range); 191 this.drawColumn(column, ray, angle, map); 192 } 193 this.ctx.restore(); 194 }; 195 196 Camera.prototype.drawWeapon = function(weapon, paces) { 197 var bobX = Math.cos(paces * 2) * this.scale * 6; 198 var bobY = Math.sin(paces * 4) * this.scale * 6; 199 var left = this.width * 0.66 + bobX; 200 var top = this.height * 0.6 + bobY; 201 this.ctx.drawImage(weapon.image, left, top, weapon.width * this.scale, weapon.height * this.scale); 202 }; 203 204 Camera.prototype.drawColumn = function(column, ray, angle, map) { 205 var ctx = this.ctx; 206 var texture = map.wallTexture; 207 var left = Math.floor(column * this.spacing); 208 var width = Math.ceil(this.spacing); 209 var hit = -1; 210 211 while (++hit < ray.length && ray[hit].height <= 0); 212 213 for (var s = ray.length - 1; s >= 0; s--) { 214 var step = ray[s]; 215 var rainDrops = Math.pow(Math.random(), 3) * s; 216 var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance); 217 218 if (s === hit) { 219 var textureX = Math.floor(texture.width * step.offset); 220 var wall = this.project(step.height, angle, step.distance); 221 222 ctx.globalAlpha = 1; 223 ctx.drawImage(texture.image, textureX, 0, 1, texture.height, left, wall.top, width, wall.height); 224 225 ctx.fillStyle = ‘#000000‘; 226 ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0); 227 ctx.fillRect(left, wall.top, width, wall.height); 228 } 229 230 ctx.fillStyle = ‘#ffffff‘; 231 ctx.globalAlpha = 0.15; 232 while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height); 233 } 234 }; 235 236 Camera.prototype.project = function(height, angle, distance) { 237 var z = distance * Math.cos(angle); 238 var wallHeight = this.height * height / z; 239 var bottom = this.height / 2 * (1 + 1 / z); 240 return { 241 top: bottom - wallHeight, 242 height: wallHeight 243 }; 244 }; 245 246 function GameLoop() { 247 this.frame = this.frame.bind(this); 248 this.lastTime = 0; 249 this.callback = function() {}; 250 } 251 252 GameLoop.prototype.start = function(callback) { 253 this.callback = callback; 254 requestAnimationFrame(this.frame); 255 }; 256 257 GameLoop.prototype.frame = function(time) { 258 var seconds = (time - this.lastTime) / 1000; 259 this.lastTime = time; 260 if (seconds < 0.2) this.callback(seconds); 261 requestAnimationFrame(this.frame); 262 }; 263 264 var display = document.getElementById(‘display‘); 265 var player = new Player(15.3, -1.2, Math.PI * 0.3); 266 var map = new Map(32); 267 var controls = new Controls(); 268 var camera = new Camera(display, MOBILE ? 160 : 320, 0.8); 269 var loop = new GameLoop(); 270 271 map.randomize(); 272 273 loop.start(function frame(seconds) { 274 map.update(seconds); 275 player.update(controls.states, map, seconds); 276 camera.render(player, map); 277 }); 278 279 </script> 280 <script> 281 (function(i,s,o,g,r,a,m){i[‘GoogleAnalyticsObject‘]=r;i[r]=i[r]||function(){ 282 (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 283 m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 284 })(window,document,‘script‘,‘//www.google-analytics.com/analytics.js‘,‘ga‘); 285 286 ga(‘create‘, ‘UA-50885475-1‘, ‘playfuljs.com‘); 287 ga(‘send‘, ‘pageview‘); 288 </script> 289 </body> 290 </html>
【转】265行JavaScript代码的第一人称3D H5游戏Demo
标签:
原文地址:http://www.cnblogs.com/muyun/p/5094992.html