http://www.php100.com/html/it/mobile/2014/0612/6971.html
玩才是学习的动力
背景分析
//一个圆圈弧度
//用来识别用户可视弧度 等等,绘制skybox时做图片截取处理.
var CIRCLE = Math.PI * 2;
var MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i
.test(navigator.userAgent)
function Controls() {
this.codes = {
37 : 'left',
39 : 'right',
38 : 'forward',
40 : 'backward'
};
this.states = {
'left' : false,
'right' : false,
'forward' : false,
'backward' : false
};
document.addEventListener('keydown', this.onKey.bind(this, true), false);
document.addEventListener('keyup', this.onKey.bind(this, false), false);
document.addEventListener('touchstart', this.onTouch.bind(this), false);
document.addEventListener('touchmove', this.onTouch.bind(this), false);
document.addEventListener('touchend', this.onTouchEnd.bind(this), false);
}
Controls.prototype.onTouch = function(e) {
var t = e.touches[0];
this.onTouchEnd(e);
if (t.pageY < window.innerHeight * 0.5)
this.onKey(true, {
keyCode : 38
});
else if (t.pageX < window.innerWidth * 0.5)
this.onKey(true, {
keyCode : 37
});
else if (t.pageY > window.innerWidth * 0.5)
this.onKey(true, {
keyCode : 39
});
};
Controls.prototype.onTouchEnd = function(e) {
this.states = {
'left' : false,
'right' : false,
'forward' : false,
'backward' : false
};
// 关闭默认事件
e.preventDefault();
// 关闭事件传递
e.stopPropagation();
};
Controls.prototype.onKey = function(val, e) {
var state = this.codes[e.keyCode];
if (typeof state === 'undefined')
return;
this.states[state] = val;
e.preventDefault && e.preventDefault();
e.stopPropagation && e.stopPropagation();
};
function Bitmap(src, width, height) {
this.image = new Image();
this.image.src = src;
this.width = width;
this.height = height;
}
/**
*
* @param x
* 坐标
* @param y
* 坐标
* @param direction
* 方向
* @returns {Player}
*/
function Player(x, y, direction) {
this.x = x;
this.y = y;
this.direction = direction;
// 武器
this.weapon = new Bitmap('knife_hand.png', 319, 320);
this.paces = 0;
}
/**
* 旋转
*
* @param angle
* 角度
*/
Player.prototype.rotate = function(angle) {
// 方向
this.direction = (this.direction + angle + CIRCLE) % (CIRCLE);
};
/**
* @param distance
* 距离
*/
Player.prototype.walk = function(distance, map) {
var dx = Math.cos(this.direction) * distance;
var dy = Math.sin(this.direction) * distance;
if (map.get(this.x + dx, this.y) <= 0)
this.x += dx;
if (map.get(this.x, this.y + dy) <= 0)
this.y += dy;
this.paces += distance;
};
Player.prototype.update = function(controls, map, seconds) {
if (controls.left)
this.rotate(-Math.PI * seconds);
if (controls.right)
this.rotate(Math.PI * seconds);
if (controls.forward)
this.walk(3 * seconds, map);
if (controls.backward)
this.walk(-3 * seconds, map);
};
function Map(size) {
// 地区大小
this.size = size;
// 地区网格大小
this.wallGrid = new Uint8Array(size * size);
// 天空画布大小
this.skybox = new Bitmap('deathvalley_panorama_sky.jpg', 4000, 1290);
// 墙体画布大小
this.wallTexture = new Bitmap('wall_texture.jpg', 1024, 1024);
this.light = 0;
}
Map.prototype.get = function(x, y) {
x = Math.floor(x);
y = Math.floor(y);
if (x < 0 || x > this.size - 1 || y < 0 || y > this.size - 1)
return -1;
return this.wallGrid[y * this.size + x];
};
Map.prototype.randomize = function() {
for ( var i = 0; i < this.size * this.size; i++) {
this.wallGrid[i] = Math.random() < 0.3 ? 1 : 0;
}
};
/**
* @point
* @angle
* @range 范围
*/
Map.prototype.cast = function(point, angle, range) {
var self = this;
var sin = Math.sin(angle);
var cos = Math.cos(angle);
var noWall = {
length2 : Infinity
};
return ray({
x : point.x,
y : point.y,
height : 0,
distance : 0
});
function ray(origin) {
var stepX = step(sin, cos, origin.x, origin.y);
var stepY = step(cos, sin, origin.y, origin.x, true);
var nextStep = stepX.length2 < stepY.length2 ? inspect(stepX, 1, 0,
origin.distance, stepX.y) : inspect(stepY, 0, 1,
origin.distance, stepY.x);
if (nextStep.distance > range)
return [ origin ];
return [ origin ].concat(ray(nextStep));
}
function step(rise, run, x, y, inverted) {
if (run === 0)
return noWall;
var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
var dy = dx * (rise / run);
return {
x : inverted ? y + dy : x + dx,
y : inverted ? x + dx : y + dy,
length2 : dx * dx + dy * dy
};
}
function inspect(step, shiftX, shiftY, distance, offset) {
var dx = cos < 0 ? shiftX : 0;
var dy = sin < 0 ? shiftY : 0;
step.height = self.get(step.x - dx, step.y - dy);
step.distance = distance + Math.sqrt(step.length2);
if (shiftX)
step.shading = cos < 0 ? 2 : 0;
else
step.shading = sin < 0 ? 2 : 1;
step.offset = offset - Math.floor(offset);
return step;
}
};
/**
* 模拟闪电的时间间隔 this.light 为当前地区的亮度值
*/
Map.prototype.update = function(seconds) {
return;
if (this.light > 0)// 按时间把亮度调低.当为最暗时,用0表示
this.light = Math.max(this.light - 10 * seconds, 0);
else if (Math.random() * 5 < seconds)
this.light = 2;// 按某概率 出现闪电
};
function Camera(canvas, resolution, fov) {
// 画布
this.ctx = canvas.getContext('2d');
// 通过style 将 画面强制放大,填充屏幕。画面越精细,速度越慢,反之则画面模糊,但是速度快。默认大小为0.5
this.width = canvas.width = window.innerWidth * 0.5;
this.height = canvas.height = window.innerHeight * 0.5;
this.resolution = resolution;
this.spacing = this.width / resolution;
// 视角 为固定 0.4*pi
this.fov = fov;
this.range = MOBILE ? 8 : 14;
this.lightRange = 5;
//
this.scale = (this.width + this.height) / 1200;
}
Camera.prototype.render = function(player, map) {
// 单纯只是计算出闭合后。图片的相关特征,做背景
this.drawSky(player.direction, map.skybox, map.light);
// 重点计算
// this.drawColumns(player, map);
// 玩家武器图,只是 一张晃动的图
// this.drawWeapon(player.weapon, player.paces);
};
/**
* direction 方向 (摄像头[用户角度方向]) sky 天空元素 (闭合状态下[图,首尾相连,形成的一圈为360度,即2*pi=CIRCLE ]。)
* ambient 环境 (遮罩层,用来降低光线)
*/
Camera.prototype.drawSky = function(direction, sky, ambient) {
// 计算出当前背景图的可视范围 this.fov(可视范围)
// 不清楚这里的计算原理。
var width = this.width * (CIRCLE / this.fov); // 2/0.4
// 旋转偏角 图片左边的部分偏移
// 宽度 direction 为当前视角弧度
var left = -width * direction / CIRCLE;
this.ctx.save();
this.ctx.drawImage(sky.image, left, 0, width, this.height);
if (left < width - this.width) {
// console.log(left,direction / CIRCLE);
this.ctx.drawImage(sky.image, left + width, 0, width, this.height);
}
// 一个遮罩层。让背景变亮或暗 , 即 map.ligth 配合闪电,做地面的渲染处理
if (ambient > 0) {
this.ctx.fillStyle = '#ffffff';
this.ctx.globalAlpha = ambient * 0.1;
this.ctx.fillRect(0, this.height * 0.5, this.width, this.height * 0.5);
}
this.ctx.restore();
};
Camera.prototype.drawColumns = function(player, map) {
this.ctx.save();
for ( var column = 0; column < this.resolution; column++) {
var angle = this.fov * (column / this.resolution - 0.5);
var ray = map.cast(player, player.direction + angle, this.range);
this.drawColumn(column, ray, angle, map);
}
this.ctx.restore();
};
Camera.prototype.drawWeapon = function(weapon, paces) {
var bobX = Math.cos(paces * 2) * this.scale * 6;
var bobY = Math.sin(paces * 4) * this.scale * 6;
var left = this.width * 0.66 + bobX;
var top = this.height * 0.6 + bobY;
this.ctx.drawImage(weapon.image, left, top, weapon.width * this.scale,
weapon.height * this.scale);
};
Camera.prototype.drawColumn = function(column, ray, angle, map) {
var ctx = this.ctx;
var texture = map.wallTexture;
var left = Math.floor(column * this.spacing);
var width = Math.ceil(this.spacing);
var hit = -1;
while (++hit < ray.length && ray[hit].height <= 0)
;
for ( var s = ray.length - 1; s >= 0; s--) {
var step = ray[s];
var rainDrops = Math.pow(Math.random(), 3) * s;
var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);
if (s === hit) {
var textureX = Math.floor(texture.width * step.offset);
var wall = this.project(step.height, angle, step.distance);
ctx.globalAlpha = 1;
ctx.drawImage(texture.image, textureX, 0, 1, texture.height, left,
wall.top, width, wall.height);
ctx.fillStyle = '#000000';
ctx.globalAlpha = Math.max((step.distance + step.shading)
/ this.lightRange - map.light, 0);
ctx.fillRect(left, wall.top, width, wall.height);
}
ctx.fillStyle = '#ffffff';
ctx.globalAlpha = 0.15;
while (--rainDrops > 0)
ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);
}
};
Camera.prototype.project = function(height, angle, distance) {
var z = distance * Math.cos(angle);
var wallHeight = this.height * height / z;
var bottom = this.height / 2 * (1 + 1 / z);
return {
top : bottom - wallHeight,
height : wallHeight
};
};
function GameLoop() {
this.frame = this.frame.bind(this);
this.lastTime = 0;
this.callback = function() {
};
}
GameLoop.prototype.start = function(callback) {
this.callback = callback;
requestAnimationFrame(this.frame);
};
GameLoop.prototype.frame = function(time) {
var seconds = (time - this.lastTime) / 1000;
this.lastTime = time;
if (seconds < 0.2)
this.callback(seconds);
requestAnimationFrame(this.frame);
};
var display = document.getElementById('display');
var player = new Player(15.3, -1.2, Math.PI * 0.3);
var map = new Map(32);
var controls = new Controls();
var camera = new Camera(display, MOBILE ? 160 : 320, Math.PI * 0.4);
var loop = new GameLoop();
map.randomize();
// 系统自动循环时,自动调整时间 seconds 时间间隔长度 .
loop.start(function frame(seconds) {
map.update(seconds);
player.update(controls.states, map, seconds);
camera.render(player, map);
});
武器分析
//一个圆圈弧度
//用来识别用户可视弧度 等等,绘制skybox时做图片截取处理.
var CIRCLE = Math.PI * 2;
var MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i
.test(navigator.userAgent)
function Controls() {
this.codes = {
37 : 'left',
39 : 'right',
38 : 'forward',
40 : 'backward'
};
this.states = {
'left' : false,
'right' : false,
'forward' : false,
'backward' : false
};
document.addEventListener('keydown', this.onKey.bind(this, true), false);
document.addEventListener('keyup', this.onKey.bind(this, false), false);
document.addEventListener('touchstart', this.onTouch.bind(this), false);
document.addEventListener('touchmove', this.onTouch.bind(this), false);
document.addEventListener('touchend', this.onTouchEnd.bind(this), false);
}
Controls.prototype.onTouch = function(e) {
var t = e.touches[0];
this.onTouchEnd(e);
if (t.pageY < window.innerHeight * 0.5)
this.onKey(true, {
keyCode : 38
});
else if (t.pageX < window.innerWidth * 0.5)
this.onKey(true, {
keyCode : 37
});
else if (t.pageY > window.innerWidth * 0.5)
this.onKey(true, {
keyCode : 39
});
};
Controls.prototype.onTouchEnd = function(e) {
this.states = {
'left' : false,
'right' : false,
'forward' : false,
'backward' : false
};
// 关闭默认事件
e.preventDefault();
// 关闭事件传递
e.stopPropagation();
};
Controls.prototype.onKey = function(val, e) {
var state = this.codes[e.keyCode];
if (typeof state === 'undefined')
return;
this.states[state] = val;
e.preventDefault && e.preventDefault();
e.stopPropagation && e.stopPropagation();
};
function Bitmap(src, width, height) {
this.image = new Image();
this.image.src = src;
this.width = width;
this.height = height;
}
/**
*
* @param x
* 坐标
* @param y
* 坐标
* @param direction
* 方向
* @returns {Player}
*/
function Player(x, y, direction) {
this.x = x;
this.y = y;
this.direction = direction;
// 武器
this.weapon = new Bitmap('knife_hand.png', 319, 320);
this.paces = 0;
}
/**
* 旋转
*
* @param angle
* 角度
*/
Player.prototype.rotate = function(angle) {
// 方向
this.direction = (this.direction + angle + CIRCLE) % (CIRCLE);
};
/**
* @param distance
* 距离
*/
Player.prototype.walk = function(distance, map) {
var dx = Math.cos(this.direction) * distance;
var dy = Math.sin(this.direction) * distance;
if (map.get(this.x + dx, this.y) <= 0)
this.x += dx;
if (map.get(this.x, this.y + dy) <= 0)
this.y += dy;
//前进的距离。步数
//旋转不算
this.paces += distance;
};
Player.prototype.update = function(controls, map, seconds) {
if (controls.left)
this.rotate(-Math.PI * seconds);
if (controls.right)
this.rotate(Math.PI * seconds);
if (controls.forward)
this.walk(3 * seconds, map);
if (controls.backward)
this.walk(-3 * seconds, map);
};
function Map(size) {
// 地区大小
this.size = size;
// 地区网格大小
this.wallGrid = new Uint8Array(size * size);
// 天空画布大小
this.skybox = new Bitmap('deathvalley_panorama.jpg', 4000, 1290);
// 墙体画布大小
this.wallTexture = new Bitmap('wall_texture.jpg', 1024, 1024);
this.light = 0;
}
Map.prototype.get = function(x, y) {
x = Math.floor(x);
y = Math.floor(y);
if (x < 0 || x > this.size - 1 || y < 0 || y > this.size - 1)
return -1;
return this.wallGrid[y * this.size + x];
};
Map.prototype.randomize = function() {
for ( var i = 0; i < this.size * this.size; i++) {
this.wallGrid[i] = Math.random() < 0.3 ? 1 : 0;
}
};
/**
* @point
* @angle
* @range 范围
*/
Map.prototype.cast = function(point, angle, range) {
var self = this;
var sin = Math.sin(angle);
var cos = Math.cos(angle);
var noWall = {
length2 : Infinity
};
return ray({
x : point.x,
y : point.y,
height : 0,
distance : 0
});
function ray(origin) {
var stepX = step(sin, cos, origin.x, origin.y);
var stepY = step(cos, sin, origin.y, origin.x, true);
var nextStep = stepX.length2 < stepY.length2 ? inspect(stepX, 1, 0,
origin.distance, stepX.y) : inspect(stepY, 0, 1,
origin.distance, stepY.x);
if (nextStep.distance > range)
return [ origin ];
return [ origin ].concat(ray(nextStep));
}
function step(rise, run, x, y, inverted) {
if (run === 0)
return noWall;
var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
var dy = dx * (rise / run);
return {
x : inverted ? y + dy : x + dx,
y : inverted ? x + dx : y + dy,
length2 : dx * dx + dy * dy
};
}
function inspect(step, shiftX, shiftY, distance, offset) {
var dx = cos < 0 ? shiftX : 0;
var dy = sin < 0 ? shiftY : 0;
step.height = self.get(step.x - dx, step.y - dy);
step.distance = distance + Math.sqrt(step.length2);
if (shiftX)
step.shading = cos < 0 ? 2 : 0;
else
step.shading = sin < 0 ? 2 : 1;
step.offset = offset - Math.floor(offset);
return step;
}
};
/**
* 模拟闪电的时间间隔
* this.light 为当前地区的亮度值
*/
Map.prototype.update = function(seconds) {
if (this.light > 0)//按时间把亮度调低.当为最暗时,用0表示
this.light = Math.max(this.light - 10 * seconds, 0);
else if (Math.random() * 5 < seconds)
this.light = 2;//按某概率 出现闪电
};
function Camera(canvas, resolution, fov) {
// 画布
this.ctx = canvas.getContext('2d');
// 通过style 将 画面强制放大,填充屏幕。画面越精细,速度越慢,反之则画面模糊,但是速度快。默认大小为0.5
this.width = canvas.width = window.innerWidth * 0.5;
this.height = canvas.height = window.innerHeight * 0.5;
//分辨率
this.resolution = resolution;
this.spacing = this.width / resolution;
// 视角 为固定 0.4*pi
this.fov = fov;
this.range = MOBILE ? 8 : 14;
this.lightRange = 5;
//只用来识别武器的分变率。或者说,大小
this.scale = (this.width + this.height) / 1200;
}
Camera.prototype.render = function(player, map) {
// 单纯只是计算出闭合后。图片的相关特征,做背景
this.drawSky(player.direction, map.skybox, map.light);
// 重点计算
this.drawColumns(player, map);
// 玩家武器图,只是 一张晃动的图
//旋转时不晃动。晃动的幅度根据当前前进的步数或者后腿时的步数
this.drawWeapon(player.weapon, player.paces);
};
/**
* direction 方向 (摄像头[用户角度方向]) sky 天空元素 (闭合状态下[图,首尾相连,形成的一圈为360度,即2*pi=CIRCLE ]。)
* ambient 环境 (遮罩层,用来降低光线)
*/
Camera.prototype.drawSky = function(direction, sky, ambient) {
// 计算出当前背景图的可视范围 this.fov(可视范围)
//不清楚这里的计算原理。
var width = this.width * (CIRCLE / this.fov); // 2/0.4
// 旋转偏角 图片左边的部分偏移
// 宽度 direction 为当前视角弧度
var left = -width * direction / CIRCLE;
this.ctx.save();
this.ctx.drawImage(sky.image, left, 0, width, this.height);
if (left < width - this.width) {
//console.log(left,direction / CIRCLE);
this.ctx.drawImage(sky.image, left + width, 0, width, this.height);
}
if (ambient > 0) {
this.ctx.fillStyle = '#ffffff';
this.ctx.globalAlpha = ambient * 0.1;
this.ctx.fillRect(0, this.height * 0.5, this.width, this.height * 0.5);
}
this.ctx.restore();
};
Camera.prototype.drawColumns = function(player, map) {
this.ctx.save();
for ( var column = 0; column < this.resolution; column++) {
var angle = this.fov * (column / this.resolution - 0.5);
var ray = map.cast(player, player.direction + angle, this.range);
this.drawColumn(column, ray, angle, map);
}
this.ctx.restore();
};
Camera.prototype.drawWeapon = function(weapon, paces) {
//利用cos 的正弦和余弦的波动范围为1至-1,加上对应的识别大小做抖动
//模拟人走动时,摆动的弧度.曲线.
var bobX = Math.cos(paces * 2) * this.scale * 6;
var bobY = Math.sin(paces * 4) * this.scale * 6;
var left = this.width * 0.66 + bobX;
var top = this.height * 0.6 + bobY;
this.ctx.drawImage(weapon.image, left, top, weapon.width * this.scale,
weapon.height * this.scale);
};
Camera.prototype.drawColumn = function(column, ray, angle, map) {
var ctx = this.ctx;
var texture = map.wallTexture;
var left = Math.floor(column * this.spacing);
var width = Math.ceil(this.spacing);
var hit = -1;
while (++hit < ray.length && ray[hit].height <= 0)
;
for ( var s = ray.length - 1; s >= 0; s--) {
var step = ray[s];
var rainDrops = Math.pow(Math.random(), 3) * s;
var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);
if (s === hit) {
var textureX = Math.floor(texture.width * step.offset);
var wall = this.project(step.height, angle, step.distance);
ctx.globalAlpha = 1;
ctx.drawImage(texture.image, textureX, 0, 1, texture.height, left,
wall.top, width, wall.height);
ctx.fillStyle = '#000000';
ctx.globalAlpha = Math.max((step.distance + step.shading)
/ this.lightRange - map.light, 0);
ctx.fillRect(left, wall.top, width, wall.height);
}
ctx.fillStyle = '#ffffff';
ctx.globalAlpha = 0.15;
while (--rainDrops > 0)
ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);
}
};
Camera.prototype.project = function(height, angle, distance) {
var z = distance * Math.cos(angle);
var wallHeight = this.height * height / z;
var bottom = this.height / 2 * (1 + 1 / z);
return {
top : bottom - wallHeight,
height : wallHeight
};
};
function GameLoop() {
this.frame = this.frame.bind(this);
this.lastTime = 0;
this.callback = function() {
};
}
GameLoop.prototype.start = function(callback) {
this.callback = callback;
requestAnimationFrame(this.frame);
};
GameLoop.prototype.frame = function(time) {
var seconds = (time - this.lastTime) / 1000;
this.lastTime = time;
if (seconds < 0.2)
this.callback(seconds);
requestAnimationFrame(this.frame);
};
var display = document.getElementById('display');
var player = new Player(15.3, -1.2, Math.PI * 0.3);
var map = new Map(32);
var controls = new Controls();
var camera = new Camera(display, MOBILE ? 160 : 320, Math.PI * 0.4);
var loop = new GameLoop();
map.randomize();
//系统自动循环时,自动调整时间 seconds 时间间隔长度 .
loop.start(function frame(seconds) {
map.update(seconds);
player.update(controls.states, map, seconds);
camera.render(player, map);
});