本文目的是分解前面的代码。其实,它得逻辑很清楚,只是对于我这种只是用过 Canvas 画线(用过 Fabric.js Canvas库)的人来说,这个还是很复杂的。我研究这个背景天空也是搞了一天,下面就是只加载天空的代码及分析。

在线效果点击:http://1.codemo2.sinaapp.com/3d_demo_265line/index.html   【可以用键盘“左右”键控制】【手机浏览器触控有些异常】

  原理大概就是:

1. 创建主循环

2. 主循环内重复调用绘制方法

3. 绘制方法: 针对 Player 的位置和方向,绘制背景图

  其中用到了 H5 的 requestAnimationFrame(callback),bind(this, argu,...) 比较难以理解的函数。

  Player 是人物,含有平面x,y位置和方向三个特性;Controls 用来响应键盘和触屏操作;Map 是背景图还有后面的墙壁;Camera 是最重要的摄像机,用来绘制我们看到的炫酷图像;GameLoop 是整个程序的入口,一直循环调用 Camera 刷新绘制图形。

一、主循环

程序入口是

var loop = new GameLoop();
loop.start(function frame(seconds){});  //将 frame(secondes) 函数赋值给 GameLoop 对象的callback

GameLoop.prototype.start = function(callback) {
        this.callback = callback;
        requestAnimationFrame(this.frame);
    }

紧接着在 loop.start() 里立即调用 requestAnimationFrame(this.frame); 通知浏览器 loop.frame 函数要播放动画。

看看 loop.frame() 里面都干了啥:

GameLoop.prototype.frame = function(time){
        var seconds = (time - this.lastTime) / 1000;
        this.lastTime = time;
        if (seconds < 0.2) this.callback(seconds);   //【this.callback在loop.start之后才被赋值为function frame(seconds){}】
        requestAnimationFrame(this.frame);  //调用自己,产生无限循环;requestAnimationFrame 用法类似 setTimeout()

}

注意:要区分清楚 this.callback(argu) 函数和 this.frame(argu) 。

其中,this.callback(seconds); 调用 传给 loop.start() 的这个 函数:

loop.start(function frame(seconds){
        player.update(controls.states, map, seconds);  //更新 player 的面向、地图/背景图
        camera.render(player, map);           //绘制 player 和地图/背景图
    });

下面就是如何更新和绘制 player 和地图/背景图了。


二、Player和地图更新及绘制

  player.update(controls.states, map, seconds);  //更新 player 的面向、地图/背景图

  player 对象去读取全局变量 controls (里面记录着用户是否点击上下左右按键或触屏事件),如果用户按了【左】键,player 的 面向就发生改变。代码:

Player.prototype.update = function(controls, map, seconds){
        if (controls.left) {this.rotate(-Math.PI * seconds)};
        if (controls.right) this.rotate(Math.PI * seconds);
    }

  其中controls 监听键盘和触控事件,遇到 keydown/keyup/touchstart等事件,调用事件响应函数,将触屏事件转化为键盘值,再转化为 Player 的【左右转动和前后移动】并更新 player 的状态。(代码多且简单,此处不列)

  接下来是真正的绘制背景了。      camera.render(player, map);           //绘制 player 和地图/背景图

  我们来看看 camera.render() 函数的实现:

 function Camera(canvas, resolution , focalLength){
        this.ctx = canvas.getContext('2d');
        this.width = canvas.width = window.innerWidth * 0.5;
        this.height = canvas.height = window.innerHeight * 0.5;
        this.resolution = resolution;
        this.spacing = this.width / resolution;
        this.focalLength = focalLength || 0.8;
        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);  //player的面向,地图背景图,地图环境光
    }

 Camera.prototype.drawSky = function(direction, sky, ambient){
        var width = sky.width * (this.height / sky.height) * 2;  //保持背景图宽高比的同时,将图重复左右拼接
        var left = (direction / CIRCLE) * -width;

this.ctx.save();
        this.ctx.drawImage(sky.image, left, 0, width, this.height);  //调用 Canvas 2d 的 drawImage()
        if (left < width - this.width) {
          this.ctx.drawImage(sky.image, left + width, 0, width, this.height);  
        }
        this.ctx.restore();
    }

  这里看不懂了。。。为何 left  是个 负数?  

  

三、背景图的拼接显示

  想了三天终于想清楚了,关键点是 HTML5 Canvas 的 drawImage(img,x,y,width,height) 函数没有理解清楚。平时使用 drawImage() 时,参数均是正数,没有思考当5个参数时 x, y 为负数时的含义。x , y 的准确意义是【在画布上的 x ,y 处定位图像】。当 x, y 为负数时,即说明在画布的 -100,-100 处开始绘制原图,简单说就是,原图的左上角被隐藏了。见图1:

<img id="tulip" src="flower.jpg" width="400" height="266" />
<canvas id="myCanvas" width="800" height="300" /> var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("tulip");
ctx.drawImage(img,-400,-133,,266);  //img,x,y,width,height
//先拉伸原图,再隐藏部分区域

//结果见图1
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("tulip");
ctx.drawImage(img,0,0,,266);  //img,x,y,width,height
//拉伸原图
//结果见图2
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("tulip");
ctx.drawImage(img,-400,0,800,266);//绘制被拉伸2倍后图片的左半边,这时画布右半边是空白
ctx.drawImage(img,,0,800,266);//接着绘制画布右半边,内容还是被拉伸后的图片
//画布上显示的结果就是原图的首尾(左右)连接了起来
//同理,本文的背景星空图也就是这样首尾连接起来的
//结果见图3

    

图1 drawImage第23参数为负隐藏部分图片   图2 drawImage的width参数为原图片两倍_自动拉伸图片  图3 使用drawImage将原图左右连接起来

回头再看看

Camera.prototype.drawSky = function(direction, sky, ambient){
        var width = sky.width * (this.height / sky.height) * 2;  //保持背景图宽高比的同时,将图重复左右拼接
        var left = (direction / CIRCLE) * -width;         

this.ctx.save();
        this.ctx.drawImage(sky.image, left, 0, width, this.height);  //调用 Canvas 2d 的 drawImage()
        if (left < width - this.width) {
          this.ctx.drawImage(sky.image, left + width, 0, width, this.height);  
        }
        this.ctx.restore();
    }

其中 CIRCLE 是定义为 2*Math.PI 的常量,direction 前面也有说明 等于 (this.direction + angle + CIRCLE) % (CIRCLE);  即永远在 0 ~ 2Pi 之间,所以 (direction / CIRCLE) 也永远在 0~1 之间,于是

left = (direction / CIRCLE) * -width 也就在 (-width , 0)之间。

下面是我用PPT画的说明图,这就能解释为何 left 为负数,width 要用原图宽度乘以2了。


 <!--

 1. draw sky

 -->

 <!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Raycaster Demo - PlayfulJS</title>
</head>
<body style='background: #000; margin: 0; padding: 0; width: 100%; height: 100%;'>
<canvas id='display' width='1' height='1' style='width: 100%; height: 100%;' /> <script> 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('toucheend', 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();
// console.log(e.keyCode);
} function Bitmap(url, width, height){
this.image = new Image();
this.image.src = url;
this.width = width;
this.height = height;
}
function Map(){
this.skybox = new Bitmap('assets/deathvalley_panorama.jpg', 2000, 750);
} function Player(x, y, direction){
this.x = x;
this.y = y;
this.direction = direction;
} //弧度制
Player.prototype.rotate = function(angle){
console.log(angle);
this.direction = (this.direction + angle + CIRCLE) % (CIRCLE);
} Player.prototype.update = function(controls, map, seconds){
if (controls.left) {this.rotate(-Math.PI * seconds)};
if (controls.right) this.rotate(Math.PI * seconds);
// console.log("sdf");
} //http://www.ituring.com.cn/article/50019
//camera renderer scene
//resolution : 分辨率
function Camera(canvas, resolution , focalLength){
this.ctx = canvas.getContext('2d');
this.width = canvas.width = window.innerWidth * 0.5;
this.height = canvas.height = window.innerHeight * 0.5;
this.resolution = resolution;
this.spacing = this.width / resolution;
this.focalLength = focalLength || 0.8;
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);
} //ambient: environment light
Camera.prototype.drawSky = function(direction, sky, ambient){
var width = sky.width * (this.height / sky.height) * 2;
var left = (direction / CIRCLE) * -width; this.ctx.save();
this.ctx.drawImage(sky.image, left, 0, width, this.height);
if (left < width - this.width) {
this.ctx.drawImage(sky.image, left + width, 0, width, this.height);
}
this.ctx.restore();
} function GameLoop(){
// this.start =
this.lastTime = 0; //control FPS
this.frame = this.frame.bind(this);
this.callback = function(){}; //place holder
} //requestAnimationFrame make borswer start animate,argu is callbadk
GameLoop.prototype.start = function(callback) {
this.callback = callback;
requestAnimationFrame(this.frame);
// body...
} 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 camera = new Camera(display, MOBILE ? 160 : 320, 0.8);
var map = new Map();
var controls = new Controls();
var loop = new GameLoop(); loop.start(function frame(seconds){
//update map
// update player
player.update(controls.states, map, seconds);
// console.log("refresh..");
camera.render(player, map);
}); </script>
</body>
</html>

参考:http://www.ituring.com.cn/article/48955#  有关3D Camera

265行JavaScript代码的第一人称3D H5游戏Demo【个人总结1】的更多相关文章

  1. 【转】265行JavaScript代码的第一人称3D H5游戏Demo

    译文:http://blog.jobbole.com/70956/ 原文:http://www.playfuljs.com/a-first-person-engine-in-265-lines/ 这是 ...

  2. 教你看懂网上流传的60行JavaScript代码俄罗斯方块游戏

    早就听说网上有人仅仅用60行JavaScript代码写出了一个俄罗斯方块游戏,最近看了看,今天在这篇文章里面我把我做的分析整理一下(主要是以注释的形式). 我用C写一个功能基本齐全的俄罗斯方块的话,大 ...

  3. 60行JavaScript代码俄罗斯方块

    教你看懂网上流传的60行JavaScript代码俄罗斯方块游戏   早就听说网上有人仅仅用60行JavaScript代码写出了一个俄罗斯方块游戏,最近看了看,今天在这篇文章里面我把我做的分析整理一下( ...

  4. 只要200行JavaScript代码,就能把特斯拉汽车带到您身边

    Jerry的前一篇文章 如何使用JavaScript开发AR(增强现实)移动应用 (一) 介绍了用React-Native + ViroReact开发增强现实应用的一些预备知识. 本文咱们开始进入增强 ...

  5. 只有20行Javascript代码!手把手教你写一个页面模板引擎

    http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...

  6. 65行 JavaScript 代码实现 Flappy Bird 游戏

    飞扬的小鸟(Flappy Bird)无疑是2014年全世界最受关注的一款游戏.这款游戏是一位来自越南河内的独立游戏开发者阮哈东开发,形式简易但难度极高的休闲游戏,很容易让人上瘾. 这里给大家分享一篇这 ...

  7. 9 行 javascript 代码获取 QQ 群成员

    昨天看到一条微博:「22 行 JavaScript 代码实现 QQ 群成员提取器」. 本着好奇心点击进去,发现没有达到效果,一是 QQ 版本升级了,二是博客里面的代码也有些繁琐. 于是自己试着写了一个 ...

  8. 关于Unity中FPS第一人称射击类游戏制作(专题十)

    当前Unity最新版本5.6.3f1,我使用的是5.5.1f1 场景搭建 1: 导入人物模型, 手持一把枪;2: 导入碎片模型;3: 创建一个平面;4: 创建一个障碍物;5: 导入人物模型;6: 配置 ...

  9. 一个256行代码的第一人称引擎(Direct2D移植版)

    这篇文章是对"a first person engine in 265 lines"[1]的一个Direct2D版的移植.看到这篇文章我立刻就想到了QUAKE,当然QUAKE使用了 ...

随机推荐

  1. c# ADO连接Access 执行Open后程序自动退出

    今天利用ADO连接Access数据库的时候遇到了前所未见的问题,Access数据库连接串,OleDbConnection,open的时候,系统就会自动关闭所有调试. 我就很纠结了,这个AccessHe ...

  2. javascript 浏览器执行断点

    在javascript代码里面有个语句可以让浏览器执行到这里的时候触发断点,这个命令就是 debugger 很好用

  3. 通过html5的range属性动态改变图片的大小

    range属性已经是很成熟的属性了,我们可以使用这个属性进行动态调整图片的宽度,其中原理在于通过不断获取range的值,并赋予给所需要的图片,进而达到动态改变图片的效果.下面贴出具体的代码,主要参照了 ...

  4. ZOJ 1076 Gene Assembly(LIS+路径打印 贪心)

    题目链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=76 题目大意:在一个DNA上,给定许多基因的起始位置和结束位置,求出这 ...

  5. cocos2dx 实现不一样的ScrollView

    原来在公司被迫加班加点赶工,用lua实现的版本:http://www.cnblogs.com/mmc1206x/p/4146911.html 后来因我个人的需要, 用C++实现了一个版本. 蓦然回首, ...

  6. ES的安装运行

    一.安装,运行 1. 检查java的版本环境 Elasticsearch依赖Java,在书写本文档的时候,推荐使用Oracle JDK 1.8.0_20 或 1.7.0_55以后的版本. 在安装Ela ...

  7. ios开发中加载的image无法显示

    昨天遇到一个较奇葩的问题,imageName加载的图片显示不出来,网上查了好多资料还是没找到解决的方法: 之前图片是放在项目中SupportingFiles文件下的,怎么加载都能显示图片,于是将图片拿 ...

  8. php session 自定义的设置测试

    <?php // ini_set('session.save_handler', 'user'); // 注意 set_session_save_handler() 一定要在 session_s ...

  9. JavaScript学习总结【8】、面向对象编程

    1.什么是面向对象编程 要理解面向对象,得先搞清楚什么是对象,首先需要明确一点这里所说的对象,不是生活中的搞男女朋友对象,面向对象就是面向着对象,换在代码中,就是一段代码相中了另一段代码,自此夜以继日 ...

  10. Linux 防火墙设置,禁止某个ip访问

    service  iptables  status        查看防火墙状态 service  iptables  start           开启防火墙 service  iptables  ...