存档

‘html5’ 分类的存档

『HTML5梦幻之旅』 – 炫酷的节日贺卡

2015年2月26日 没有评论

刚过完春节,想必大家收到了各种祝福和贺卡吧~Y某我今年也为同学和家人准备了贺卡。不一样的是,我的贺卡可不是made from树,而是一行行代码凝聚而来的。

考虑到本次开发需要的功能不多,所以就没有用库件了,利用纯Html5 Canvas API来完成本次梦幻之旅:节日贺卡。虽然用到的Canvas API不多,但是效果还是蛮理想的~

首先上截图吧:

『HTML5梦幻之旅』 - 炫酷的节日贺卡

『HTML5梦幻之旅』 - 炫酷的节日贺卡

『HTML5梦幻之旅』 - 炫酷的节日贺卡

哎呀,看到了截图,各位是不是领悟了传说中的炫酷华丽(luàn qī bā zāo)?

测试地址:http://wyh.wjjsoft.com/demo/greeting_card/

大家可以先到测试地址里体验一下玩法,顺便观察一下这些小正方形所组成的文字有什么特点。

一,原理

每次写博客和大家分享技术的时候,我都会先把原理介绍给大家,因为这样一来,大家对下文中的代码理解起来就快多了。所以原理很重要,得作为第一个研究话题。

无论是在测试地址里还是截图中,都不难发现这些文字的特点:是由小正方形拼接而成的,如果说一个小方块是1px,那么这里的文字像不像像素游戏里的文字?

如何实现这样的文字呢?我们不妨先从画像素图说起。首先,我们知道图片都是由一像素一像素组成的,如果4px*4px的一张白色图片可以看成数组,那么这个数组可以表示为这样:

[
[#FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF],
[#FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF],
[#FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF],
[#FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF]
]

如果把第一排第一列的那个像素涂成黑色,那么数组变成:

[
[#000000, #FFFFFF, #FFFFFF, #FFFFFF],
[#FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF],
[#FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF],
[#FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF]
]

为了简化开发,我们设黑色时数组里表示为true,白色为false,那么上述数组又变成:

[
[true, false, false, false],
[false, false, false, false],
[false, false, false, false],
[false, false, false, false]
]

如果在这张图上画个11的字样,则数组可以表示为

[
[true, false, false, true],
[true, false, false, true],
[true, false, false, true],
[true, false, false, true]
]

可见如果我们通过数组存取文字的各像素颜色,然后遍历这个数组来获取哪些点画黑色哪些点画白色,并根据得到的颜色在界面上绘制,就能得出想要的图案。本次开发的原理就是和此类似。在demo中,粒子(在这里定义为由多个小正方形组成带有拖尾或者正在原地旋转的一个显示对象)的颜色是我随机取出的,所以在数组里,我们只用记载哪里该有一个粒子,哪里不该有。然后在添加喷射出的粒子时,我们记录下点击的位置并在剩余需要有粒子的位置列表中随机找到这个粒子该到的位置,再随机取出一个颜色并按照该颜色调用Canvas API进行渲染即可。

在本次开发中,记载哪里该有粒子哪里该是空白的数组保存在list.js中(true表示有粒子,false表示没有粒子):

var list = [[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[false,false,true,false,false,false,true,false,false,false,false,false,true,false,false,false,false,false,true,true,true,true,true,false,true,true,true,true,true,false,true,true,false,false,false,true,false,false,false,false],[false,false,true,false,false,false,true,false,false,false,false,true,false,true,false,false,false,false,true,false,false,false,true,false,true,false,false,false,true,false,false,true,false,false,false,true,false,false,false,false],[false,false,true,false,false,false,true,false,false,false,true,false,false,false,true,false,false,false,true,false,false,false,true,false,true,false,false,false,true,false,false,true,false,false,false,true,false,false,false,false],[false,false,true,true,true,true,true,false,false,false,true,false,false,false,true,false,false,false,true,false,false,false,true,false,true,false,false,false,true,false,false,true,false,false,false,true,false,false,false,false],[false,false,true,false,false,false,true,false,false,true,true,true,true,true,true,true,false,false,true,true,true,true,true,false,true,true,true,true,true,false,false,true,true,true,true,true,false,false,false,false],[false,false,true,false,false,false,true,false,false,true,false,false,false,false,false,true,false,false,true,false,false,false,false,false,true,false,false,false,false,false,false,false,false,false,false,true,false,false,false,false],[false,false,true,false,false,false,true,false,true,true,false,false,false,false,false,true,true,false,true,false,false,false,false,false,true,false,false,false,false,false,false,false,false,false,false,true,false,false,false,false],[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,false,false,false,false,false,true,false,false,false,false,false,false,true,false,false,false,true,false,false,false,false],[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,false,false,false,false,false,true,false,false,false,false,false,false,true,true,true,true,true,false,false,false,false],[false,false,false,false,true,false,false,false,false,true,false,true,true,true,true,false,true,false,false,false,false,false,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[false,false,false,true,false,true,false,false,false,true,false,true,false,false,false,false,true,false,false,false,false,false,true,false,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false],[false,false,false,true,false,true,false,false,true,false,false,true,true,true,true,false,false,true,false,true,false,true,false,false,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false],[false,false,true,false,false,false,true,false,true,false,false,true,false,false,false,false,false,true,false,true,false,true,false,false,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false],[false,false,true,false,false,false,false,true,false,false,false,true,true,true,true,false,false,false,true,false,true,false,false,false,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false],[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false],[false,false,true,false,false,true,false,true,true,true,true,false,false,false,false,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false],[false,false,true,false,false,true,false,true,false,false,false,false,false,false,true,false,true,false,false,false,true,false,false,true,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false],[false,false,true,true,true,true,false,true,true,true,true,false,false,true,false,false,false,true,false,false,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false],[false,false,false,false,false,true,false,true,false,false,false,false,false,true,true,true,true,true,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[false,false,true,true,true,true,false,true,true,true,true,false,true,true,false,false,false,true,true,false,true,false,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false],[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false],[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]];

这么大一堆数组就完成了“Happy New Year!”的字样。

为了方便,我做了一个编辑文字的工具,在线使用地址:

http://wyh.wjjsoft.com/demo/greeting_card/export_array_tool.html

使用方法很简单,就是在画板上用鼠标点击格子。灰黑色的格子代表在demo中有粒子,白色则相反。

『HTML5梦幻之旅』 - 炫酷的节日贺卡

上图就代表demo中的粒子需要构成“2015”的字样。点击“Export”按钮生成数组,然后把数组复制粘贴到list.js中,并保存到list变量下即可。

Ok,万事俱备只欠代码了。

二,代码一览

(1) index.html、common.js和Main.js

先来看html代码:

<!DOCTYPE html>
<html>
<head>
	<title>Greeting Card</title>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
	<script type="text/javascript" src="./Particle.js"></script>
	<script type="text/javascript" src="./Sprite.js"></script>
	<script type="text/javascript" src="./Txt.js"></script>
	<script type="text/javascript" src="./Stage.js"></script>
	<script type="text/javascript" src="./Main.js"></script>
	<script type="text/javascript" src="./list.js"></script>
	<script type="text/javascript" src="./common.js"></script>
</head>
<body style="margin: 0px; padding: 0px; font-size: 0px">
	<canvas id="mycanvas"></canvas>
</body>
</html>

为了方便上文所提到的文字编辑器和demo相通(如画布的大小和文字画板大小相同,粒子中小正方形大小和画板格子大小相同),我们准备个common.js来保存这些常量:

var particleW = particleH = 20,
angleToRad = Math.PI / 180,
stageW = 800,
stageH = 480;

particleW和particleH分别代表小正方形宽度、高度;angleToRad是角度和弧度换算率;stageW,stageH是指舞台的宽度和高度。

然后再是Main.js。Main.js主要是用来处理事件,循环渲染,全屏显示以及实例化舞台和文本并提供了获取哪些地方该有粒子哪些地方不该有粒子的函数接口,当然还有其他的功能不一一列举了,大伙就直接拿着代码啃吧。代码如下:

window.addEventListener("load", main, false);

var canvasTag, ctx;
var canvasStyleWidth, canvasStyleHeight, marginLeft = 0, marginTop = 0;
var isFirefox = true, mobile = false;
var instructionsTxt;
var instructionsIndex = 0, instructionsContents = [
	"Tap to open my greeting card~",
	"Well, continue~",
	"Don't stop tapping until you know my meaning ^_^"
];
var showList = new Array();
var positionList = new Array();

(function (n) {
	isFirefox = (n.toLowerCase().indexOf('firefox') >= 0);

	if (
		n.indexOf("iPhone") > 0
		|| n.indexOf("iPad") > 0
		|| n.indexOf("iPod") > 0
		|| n.indexOf("Android") > 0
		|| n.indexOf("Windows Phone") > 0
		|| n.indexOf("BlackBerry") > 0
	) {
		mobile = true;
	}
})(navigator.userAgent);

function main () {
	canvasTag = document.getElementById("mycanvas");
	canvasTag.width = stageW;
	canvasTag.height = stageH;
	ctx = canvasTag.getContext("2d");

	fullScreen();
	addStage();
	addInstructions();
	getParticlesPosition();
	canvasTag.addEventListener(
		"mouseup",
		function (e) {
			if (instructionsIndex++ >= instructionsContents.length - 1) {
				instructionsTxt.visible = false;
			} else {
				instructionsTxt.text = instructionsContents[instructionsIndex];
			}

			if (e.offsetX == null && e.layerX != null) {
				e.offsetX = e.layerX;
				e.offsetY = e.layerY;
			}

			var startX = scaleOffsetX(e.offsetX),
			startY = scaleOffsetY(e.offsetY);

			for (var i = 0; i < 5; i++) {
				addParticle(startX, startY);
			}
		},
		false
	);

	setInterval(function () {
		loop();
	}, 1000/60);
}

function fullScreen () {
	var w = stageW, h = stageH, ww = window.innerWidth, wh = window.innerHeight;

	if (mobile) {
		if (ww / wh > stageW / stageH) {
			h = wh;
			w = stageW * wh / stageH;
		} else {
			w = ww;
			h = stageH * ww / stageW;
		}
	}

	canvasTag.style.width = w + "px";
	canvasTag.style.height = h + "px";
	canvasTag.style.marginLeft = (ww - w) / 2 + "px";
	canvasTag.style.marginTop = (wh - h) / 2 + "px";

	canvasStyleWidth = w;
	canvasStyleHeight = h;

	if (isFirefox) {
		marginLeft = parseInt(canvasTag.style.marginLeft);
		marginTop = parseInt(canvasTag.style.marginTop);
	}
}

function addStage () {
	var stage = new Stage();
	showList.push(stage);
}

function addInstructions () {
	instructionsTxt = new Txt(instructionsContents[instructionsIndex]);
	showList.push(instructionsTxt);
}

function getParticlesPosition () {
	for (var i = 0, l = list.length; i < l; i++) {
		var item = list[i];

		for (var j = 0, n = item.length; j < n; j++) {
			if (item[j]) {
				positionList.push({x : j * particleW, y : i * particleH});
			}
		}
	}
}

function addParticle (startX, startY) {
	var index = Math.floor(Math.random() * (positionList.length - 1)),
	pos = positionList[index];

	if (!pos) {
		return;
	}

	var particle = new Particle(startX, startY, pos.x, pos.y);
	showList.push(particle);

	positionList.splice(index, 1);
}

function scaleOffsetX (v) {
	return (v - marginLeft) * stageW / canvasStyleWidth;
}

function scaleOffsetY (v) {
	return (v - marginTop) * stageH / canvasStyleHeight;
}

function loop () {
	ctx.clearRect(0, 0, canvasTag.width, canvasTag.height);

	for (var i = 0, l = showList.length; i < l; i++) {
		showList[i].loop();
	}
}

由于要放到移动端运行,所以在针对移动端做了点处理。首先介绍几个全局变量:

canvasTag 通过document.getElementById取出的一个canvas标签对象

ctx canvasTag.getContext获取的一个CanvasRenderingContext2D对象

canvasStyleWidth、canvasStyleHeight 这两个变量分别记载canvasTag的style属性中这是的width和height;用于处理全屏拉伸后,鼠标事件失灵的问题

marginLeft、marginTop 记载canvasTag的style中marginLeft,marginTop;用于处理Firefox等浏览器中,在canvasTag的位置移动后鼠标事件取出的layerX和layerY不是相对画布左上角坐标的问题(换句话说就是让点击的位置成为粒子发射的位置)

isFirefox、mobile 这俩是用来判断是否是Firefox浏览器和移动端的变量

instructionsTxt 这个是一个Txt对象,负责显示说明文本(说明文本是什么?@_@!就是demo一开始那个蛊惑你点击屏幕的家伙)

instructionsIndex、instructionsContents 前者是记载说明到了那一步,后者是记载说明内容的一个数组

showList 显示列表,把需要渲染的对象扔进这个数组,就可以使该对象重复渲染了

positionList 记载哪些地方可以出现粒子

呼~全局变量终于介绍完了,继续往下看:

(function (n) {
	isFirefox = (n.toLowerCase().indexOf('firefox') >= 0);

	if (
		n.indexOf("iPhone") > 0
		|| n.indexOf("iPad") > 0
		|| n.indexOf("iPod") > 0
		|| n.indexOf("Android") > 0
		|| n.indexOf("Windows Phone") > 0
		|| n.indexOf("BlackBerry") > 0
	) {
		mobile = true;
	}
})(navigator.userAgent);

在这个匿名函数里给isFirefox和mobile赋值。

在接下来的main函数中,任劳任怨的main需要调用全屏显示函数,加入舞台函数,加入说明文本函数,获取粒子位置函数,加入事件函数,并且还要驱动循环渲染。

值得一看的是事件部分:

canvasTag.addEventListener(
	"mouseup",
	function (e) {
		if (instructionsIndex++ >= instructionsContents.length - 1) {
			instructionsTxt.visible = false;
		} else {
			instructionsTxt.text = instructionsContents[instructionsIndex];
		}

		if (e.offsetX == null && e.layerX != null) {
			e.offsetX = e.layerX;
			e.offsetY = e.layerY;
		}

		var startX = scaleOffsetX(e.offsetX),
		startY = scaleOffsetY(e.offsetY);

		for (var i = 0; i < 5; i++) {
			addParticle(startX, startY);
		}
	},
	false
);

在某些浏览器中获取点击位置不是用offsetX和offsetY而是layerX和layerY。所以在这里我们需要统一一下。由于在移动端我做了全屏拉伸处理,所以用scaleOffsetX和scaleOffsetY来矫正鼠标位置。

(2) Stage.js和Txt.js

Stage这个类在Main.js中得到实例,并加入显示列表。Stage类的代码如下:

function Stage () {
	var s = this;

	s.width = canvasTag.width;
	s.height = canvasTag.height;
	s.bgColor = ctx.createRadialGradient(s.width / 2, s.height / 2, 10, s.width / 2, s.height / 2, s.width * 0.6);
	s.bgColor.addColorStop(0.3, "#CCCCCC");
	s.bgColor.addColorStop(1.0, "#FFFFFF");
}

Stage.prototype = {
	loop : function() {
		var s = this;

		ctx.save();
		ctx.beginPath();
		ctx.fillStyle = s.bgColor;
		ctx.rect(0, 0, s.width, s.height);
		ctx.fill();
		if (s.isShowInstructions) {
			ctx.fillStyle = "black";
			ctx.font = "bold 20px sans-serif";
			ctx.textAlign = "center";
			ctx.textBaseline = "middle";
			ctx.fillText("Tap to open greeting card~", stageCenterX, stageCenterY);
		}
		ctx.restore();
	}
};

在loop函数中,我们进行渲染,把舞台渲染出来。

再是Txt.js:

function Txt (text) {
	var s = this;

	s.x = stageW / 2;
	s.y = stageH / 2;
	s.text = text || "";
	s.visible = true;
}

Txt.prototype = {
	loop : function() {
		var s = this;

		if (!s.visible) {
			return;
		}

		ctx.save();
		ctx.fillStyle = "black";
		ctx.font = "bold 20pt sans-serif";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillText(s.text, s.x, s.y);
		ctx.restore();
	}
};

和Stage类似,通过loop来进行渲染。不同的是多了个visible属性来控制是否显示。毕竟Txt对象在demo中是可以消失的。

(3) Particle.js和Sprite.js

前面也介绍了,Particle是指许多小正方形组成的一个有拖尾的显示对象。而小正方形就是Sprite了。

先来看看Particle的代码:

function Particle (startX, startY, endX, endY) {
	var s = this;

	s.x = startX;
	s.y = startY;
	s.rotation = 0;
	s.endX = endX;
	s.endY = endY;
	s.displacement = Math.sqrt((startX - endX) * (startX - endX) + (startY - endY) * (startY - endY));
	s.stepLength = 7;
	s.stepNum = s.displacement / s.stepLength;
	s.stepIndex = 0;
	s.stopAddSprite = false;
	s.moveCos = (endX - startX) / s.displacement;
	s.moveSin = (endY - startY) / s.displacement;
	s.childList = new Array();
	s.removeList = new Array();
	s.color = Particle.COLOR_LIST[Math.round(Math.random() * (Particle.COLOR_LIST.length - 1))];
}

Particle.COLOR_LIST = [
	"#990000",
	"#FF0000",
	"#CC3300",
	"#CC6600",
	"#CC0033",
	"#FFFF00",
	"#33FF00",
	"#33CC00",
	"#0066FF",
	"#00FF99",
	"#330099",
	"#990033",
	"#000099"
];

Particle.prototype = {
	loop : function () {
		var s = this;

		s.loopChild();
		s.clearRemoveList();
		s.updateShowProperites();
		s.addChildSprite();
	},

	loopChild : function () {
		var s = this;

		for (var i = 0, l = s.childList.length; i < l; i++) {
			var o = s.childList[i];

			if (!o) {
				continue;
			}

			o.loop();

			if (o.mode == Sprite.MODE_DISAPPEAR) {
				s.removeList.push(o);
			}
		}
	},

	clearRemoveList : function () {
		var s = this;

		for (var j = 0, m = s.removeList.length; j < m; j++) {
			var toRemoveObj = s.removeList[j];

			for (var k = 0, n = s.childList.length; k < n; k++) {
				if (s.childList[k].index == toRemoveObj.index) {
					s.childList.splice(k, 1);

					break;
				}
			}
		}

		s.removeList.splice(0, s.removeList.length);
	},

	updateShowProperites : function () {
		var s = this;

		s.x += s.stepLength * s.moveCos;
		s.y += s.stepLength * s.moveSin;
		s.rotation += 10;
	},

	addChildSprite : function () {
		var s = this;

		if (s.stopAddSprite) {
			return;
		}

		if (++s.stepIndex >= s.stepNum) {
			s.x = s.endX;
			s.y = s.endY;

			var sprite = new Sprite(s.x, s.y, s.rotation, true);
			sprite.color = s.color;
			s.childList.push(sprite);

			s.stopAddSprite = true;

			return;
		}

		var sprite = new Sprite(s.x, s.y, s.rotation, false);
		sprite.color = s.color;
		s.childList.push(sprite);
	}
};

这个类就相对于前几个要复杂一些了,接受四个参数,分别是[开始x坐标, 开始y坐标, 终点x坐标, 终点y坐标]。首先是对其属性的介绍:

x、y、rotation 分别表示x坐标,y坐标,旋转角度

endX、endY 记录粒子要到的位置

displacement 传说中物理学里的位移!!!(根据高中物理必修一的知识,位移是矢量,但是这里我直接赋值为标量了,祈祷俺的物理老师没有看到这里吧)

stepLength 粒子每次移动时,移动的长度

stepNum 计算一下粒子移动到目的地需要多少步

stepIndex 粒子已经移动了多少步,用于判断粒子是否该停下了

stopAddSprite 用于判断是否停止添加拖尾小正方形

moveCos、moveSin displacement代表起始点和终点中点连线的长度,stepLength的方向也是沿着该线的。我们移动对象只能移动x,y坐标,所以计算出该线的cos和sin值以便算出x方向上的增量和y方向上的增量,从而达到按任意角度移动的对象。

childList 成员列表,装载小正方形拖尾的数组

removeList 小正方形的透明度降为0或小于0后,需要移除这些小正方形,所以把这些小正方形加到移除列表removeList中,然后等渲染完毕后遍历移除列表,从childList中移除需要移除的对象。

color 粒子的颜色,是从Particle.COLOR_LIST中随机取出的

Ok,再来看看成员函数介绍,具体的代码大家对照看吧,我只介绍一下函数执行的逻辑和功能:

loop 这个函数作为其他函数的入口

loopChild 从childList中取出小正方形Sprite对象进行渲染,并把透明度为0或小于0的小正方形放入removeList中。需要注意的是,我判断小正方形是否需要移除使用的是Sprite的mode属性,其实这个mode属性在Sprite透明度变为0或小于0时就会变成Sprite.MODE_DISAPPEAR,具体的代码见下文Sprite部分。

clearRemoveList 进行清理需要移除的小正方形。为什么不直接在loopChild函数里进行移除操作而要准备个移除列表在渲染完成后进行移除呢?那是因为你在循环渲染时,是在遍历childList,如果立刻删除需要删除的元素,就会破坏开始遍历时childList的结构,这样一来就可能出现小正方形闪烁的现象。

updateShowProperites 更新粒子的位置和旋转的角度

addChildSprite 加入小正方形实现拖尾效果

至此,Particle介绍完毕。由代码可知,Particle并没有进行渲染,而是通过childList中的Sprite对象来进行的。所以该到介绍Sprite的时候了:

function Sprite (x, y, rotation, cannotDisappear) {
	var s = this;

	s.x = x;
	s.y = y;
	s.index = Sprite.INDEX++;
	s.rotation = rotation;
	s.alpha = 1;
	s.mode = Sprite.MODE_APPEAR;
	s.cannotDisappear = cannotDisappear;
	s.color = "#FFFFFF";
	s.startDrawX = -particleW / 2;
	s.startDrawY = -particleH / 2;
}

Sprite.INDEX = 0;

Sprite.MODE_APPEAR = "appear";
Sprite.MODE_DISAPPEAR = "disappear";

Sprite.prototype = {
	loop : function () {
		var s = this;

		ctx.save();
		ctx.beginPath();
		ctx.translate(s.x, s.y);
		ctx.rotate(s.rotation * angleToRad);
		ctx.globalAlpha = s.alpha;
		ctx.rect(s.startDrawX, s.startDrawY, particleW, particleH);
		ctx.fillStyle = s.color;
		ctx.fill();
		ctx.restore();

		if (s.cannotDisappear) {
			s.rotation += 5;

			return;
		}

		s.alpha -= 0.05;

		if (s.alpha <= 0) {
			s.mode = Sprite.MODE_DISAPPEAR;
		}
	}
};

参数介绍:

x 绘制的x坐标

y 绘制的y坐标

rotation 旋转角度

cannotDisppear 是否减少透明度并可以被移除。如果该Sprite作为的是拖尾中的小正方形那么这个参数为false,如果是停止后原地旋转的小正方形则为true

属性介绍:

x、y、rotation 同Particle类的x、y、rotation

index 对象编号,移除Sprite时会用到,相当于对象的身份证

alpha 透明度

mode 为Sprite.MODE_APPEAR、Sprite.MODE_DISAPPEAR;前者表示正常显示,后者表示透明度降为0或0以下,需要移除此对象

color 小正方形颜色,由装载它的Particle的color决定

startDrawX、startDrawY 由于小正方形旋转时是按中心旋转的,所以绘制矩形时其实坐标不能为0,而是由这两个属性决定矩形的起始点

cannotDisppear 见参数cannotDisppear

函数介绍:

loop 渲染函数

最后,奉上源代码下载地址:

http://wyh.wjjsoft.com/downloads/greeting_card.zip

本章就到此为止了。欢迎大家交流~

—————————————————————-

欢迎大家转载我的文章。

转载请注明:转自Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

分类: html5 标签:

『HTML5梦幻之旅』 – 舞动色彩,Canvas下实现颜色动画

2015年1月28日 没有评论

注:为了方便起见,本次开发用到了开源引擎lufylegend,官方地址如下:http://lufylegend.com/lufylegend

今天来学习下HTML5 Canvas颜色动画。什么是颜色动画呢?以我的理解就是以某种颜色过渡到另一种颜色。和这个效果有点类似:http://w3school.com.cn/tiy/t.asp?f=css3_animation1 上面的demo是用css3实现,而我们今天要用的是Canvas。Canvas并没有相关的API,所以要想实现这种效果,只有靠自己了。

从上面的例子可以看出,我们需要完成缓动动画和颜色变化这两个基本效果,再将这两个效果组合一下,就是我们要的结果了。

1,参考资料

值得高兴的是,lufylegend为我们提供了缓动类,可以完美地实现缓动动画。所以我们现在只需要实现颜色变化了。怎么变化颜色呢?纵观Canvas所有API,我发现像素操作也许能派上用场。恰巧,lufylegend在1.9.4和1.9.4以上的版本中提供了LColorTransform这个类,用于颜色值变幻。

以下是lufylegend API文档中LColorTransform用法:http://lufylegend.com/api/zh_CN/out/classes/LColorTransform.html

这个类可以配合LBitmapData的draw,colorTransform函数使用,这两个函数的用法可参考如下给出的地址。

draw:http://lufylegend.com/api/zh_CN/out/classes/LBitmapData.html#method_draw

colorTransform:http://lufylegend.com/api/zh_CN/out/classes/LBitmapData.html#method_colorTransform

另外,各位读者还需要了解LTweenLite:http://lufylegend.com/api/zh_CN/out/classes/LTweenLite.html

本次开发会使用draw函数和LTweenLite缓动类,请仔细阅读相关函数介绍,以便阅读下文时更轻松。

2,原理

实现颜色效果的原理其实很简单,就是通过缓动类作为驱动,不断改变颜色的RGB值。

在LColorTranform中,提供了redOffset,greenOffset,blueOffset这三个属性,分别用于像素处理时对R,G,B的调整。所以,在缓动类中,我们要缓动的属性就是这三个属性。然后在onUpdate中调用LBitmapData的draw函数对显示对象刷新。

3,代码展示

现在我先把代码展示一下:

<!DOCTYPE html>
<html>
<head>
	<title>Color Transform</title>
	<script type="text/javascript" src="./lufylegend-1.9.7.min.js"></script>
	<script type="text/javascript">
		LInit(50, "mylegend", 600, 400, main);

		function main () {
			var loader = new LLoader();
			loader.addEventListener(LEvent.COMPLETE, function (e) {
				rectColorTransform();
				imageColorTransform(e.target);
			});
			loader.load("./yorhomwang.png")
		}

		function rectColorTransform () {
			var w = 200, h = 240;

			var rectLayer = new LShape();
			rectLayer.graphics.drawRoundRect(0, "", [0, 0, w, h, 5], true, "#000000");

			var bmpd = new LBitmapData(null, 0, 0, w, h);
			var bmp = new LBitmap(bmpd);
			bmp.x = bmp.y = 20;
			addChild(bmp);

			var rect = new LRectangle(0, 0, w, h);
			var ct = new LColorTransform(1, 1, 1, 1, 255, 0, 0, 0);

			startTween(bmpd, rectLayer, ct, rect);
		}

		function imageColorTransform(content) {
			var bmpd = new LBitmapData(null, 0, 0, content.width, content.height);
			var bmp = new LBitmap(bmpd);
			bmp.x = 250;
			bmp.y = 20;
			addChild(bmp);

			var rect = new LRectangle(0, 0, bmpd.width, bmpd.height);
			var ct = new LColorTransform(1, 1, 1, 1, 0, 0, 0, 0);

			startTween(bmpd, new LBitmap(new LBitmapData(content, 0, 0, rect.width, rect.height)), ct, rect);
		}

		function startTween (bmpd, layer, ct, rect) {
			var update = function (o) {
				bmpd.draw(layer, null, o, null, rect);	
			};

			update(ct);

			LTweenLite.to(ct, 3, {
				blueOffset : 255,
				loop : true,
				onUpdate : update
			}).to(ct, 3, {
				redOffset : -255,
			}).to(ct, 3, {
				greenOffset : 255,
			}).to(ct, 3, {
				blueOffset : -255,
			}).to(ct, 3, {
				redOffset : 255,
			}).to(ct, 3, {
				greenOffset : -255,
			});
		}
	</script>
</head>
<body>
	<div id="mylegend"></div>
</body>
</html>

加上html代码,不足100行,可见,LColorTransform配合LTweenLite实现颜色动画还是很简单的。

运行上面的代码,得到如下效果:

『HTML5梦幻之旅』 - 舞动色彩,Canvas下实现颜色动画

『HTML5梦幻之旅』 - 舞动色彩,Canvas下实现颜色动画

测试地址:http://wyh.wjjsoft.com/demo/color_transform/

源代码已全部给出,大家直接复制粘贴吧~


由于用到了像素处理,所以在某些电脑上运行起来可能会很卡。但是在HTML5迅猛发展的时代里,相信Canvas渲染效率的大幅提升计日可待 。

经测试,在Chrome里运行效果会比其他浏览器好得多。

—————————————————————-

欢迎大家转载我的文章。

转载请注明:转自Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

分类: html5 标签:

『HTML5梦幻之旅』 – 跟随歌曲显示当前歌词

2015年1月3日 没有评论

好像哪位老师曾说过,音乐是世界通用语言。是的,听不懂英文,但总能欣赏英文歌曲吧。

很早以前就想做个音乐播放器,但是由于跟随歌曲显示当前歌词的效果一直实现不了,所以我的想法一直无法实现。不过,最近创意不佳,没心情开发游戏了,于是闲下来搞点小发明。这次就先模仿一下手机QQ音乐中歌词显示效果。

恰巧,年末新歌蛮多的,于是我就选了一首比较好听的歌曲——手写的从前。

先看本次演示截图:

『HTML5梦幻之旅』 - 跟随歌曲显示当前歌词

演示地址:http://wyh.wjjsoft.com/demo/lyrics/

上面的演示地址可能有一半以上的人都打不开,因为这首虽然很好听,但是啊,有11MB.……建议大家要听的话还是去网上自己找吧。

好了不扯远了,进入主题吧~

一,解读歌词文件

一般而言,歌词文件的格式都是一个时间对一句歌词的,例如:

[ti:手写的从前]
[ar:周杰伦]
[t_time:(04:57)]
[00:03.23]周杰伦 - 手写的从前
[00:06.56]词:方文山 曲:周杰伦
[00:11.43]这风铃跟心动很接近 这封信还在怀念旅行
[00:21.80]路过的爱情都太年轻 你是我想要再回去的风景
[00:31.79]这别离被瓶装成秘密 这雏菊美的像诗句
[00:39.30]而我在风中等你的消息 等月光落雪地
[00:48.92]等枫红染秋季等相遇 我重温午后的阳光
[00:58.24]将吉他斜背在肩上 跟多年前一样
[01:05.71]我们轻轻的唱 去任何地方
[01:14.54]我看着你的脸 轻刷著和弦
[01:20.00]情人节卡片 手写的永远
[01:24.05]还记得广场公园 一起表演
[01:29.27]校园旁糖果店 记忆里在微甜
[01:34.99]我看着你的脸 轻刷著和弦
[01:40.31]初恋是整遍手写的从前
[01:44.50]还记得 那年秋天说了再见
[01:49.51]当恋情已走远 我将你深埋在心里面
[02:16.25]微风需要竹林 溪流需要蜻蜓
[02:18.59]乡愁般的离开 需要片片浮萍
[02:21.17]记得那年的雨季 回忆里特安静
[02:23.84]哭过后的决定 是否还能进行
[02:26.52]我傻傻等待 傻傻等春暖花开
[02:28.80]等终等于等明等白 等爱情回来
[02:31.40]青春属于表白 阳光属于窗台
[02:33.95]而我想我属于 一个拥有你的未来
…………………………
[03:39.42]我重温午后的阳光
[03:44.10]将吉他斜背在肩上 跟多年前一样
[03:51.54]我们轻轻的唱 去任何地方
[04:00.53]我看着你的脸 轻刷著和弦
[04:05.98]情人节卡片 手写的永远
[04:10.15]还记得广场公园 一起表演
[04:15.03]校园旁糖果店 记忆里在微甜
[04:20.97]我看着你的脸 轻刷著和弦
[04:26.26]初恋是整遍手写的从前
[04:30.44]还记得 那年秋天说了再见
[04:35.50]当恋情已走远 我将你深埋在心里面

不难发现,在这里,除了前三行,方括号代表的不是区间而是时间。在前三行中,方括号里的内容分别代表:歌曲名,演唱者,音乐长度。但是这些算不上歌词吧,所以真正要处理的就是前三行后的内容。但是看上去要解析这些东西会很麻烦,那怎么办呢?想用正则表达式,正则又不熟悉……于是我只好想些歪门邪道的方法了。

其实要解析一种格式,说白了就是找规律。找规律嘛,听说测智商时就要看找规律的能力……

前面也说了,格式大致就是一个时间对应一句歌词,简化一下就是:[time] lyrics [time2] lyrics2 …

经过我努力地挖掘规律,终于发现了其中的奥秘:

1)我们先把所有歌词后面/n给去掉,这样一来,歌词就连成了一排;

2)每一条时间+歌词之间通过“[”来分隔;

3)每一条中的时间和歌词之间通过“]”来分割;

有了这些发现,接下来的工作就是把这些字符串转换成程序里的数据结构。这里我准备选择了JSON作为装载数据的结构。有了这个JSON,我们要通过时间取歌词就可以直接到这个JSON中找了。

OK,歌词这种肤浅的东西就被我粗略地解读了。该上代码了。

二,手写的代码

先来看最基本的HTML代码。为了方便,本次开发用到了lufylegend.js,希望了解此引擎的朋友可以看看我以前的文章,大多数都是关于它的。

<!DOCTYPE html>
<html>
<head>
	<title>Lyrics</title>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
	<script type="text/javascript" src="./lib/lufylegend-1.9.7.simple.min.js"></script>
	<script type="text/javascript" src="./lib/lufylegend.LoadingSample1-0.1.0.min.js"></script>
	<script type="text/javascript" src="./js/ytMain.js"></script>
</head>
<body>
<div id="mylegend"></div>
</body>
</html>

我们还用到了ytMain.js这个文件,在这个文件里,首先是初始化引擎和一系列准备工作:

LInit(50, "mylegend", 350, 550, main);

var datalist = {};
var music = null;

function main () {
	document.body.style.background = "black";
	document.body.style.margin = "0px";
	document.body.style.padding = "0px";
	if (LGlobal.mobile) {
		LGlobal.stageScale = LStageScaleMode.SHOW_ALL;
	}
	LGlobal.screen(LGlobal.FULL_SCREEN);

	var loadData = [
		{path : "./js/ytLyrics.js"},

		{name : "pic", path : "./resource/albumpic.jpg"},
		{name : "lyrics", path : "./resource/shou_xie_de_cong_qian.js"},
		{name : "music", path : "./resource/shou_xie_de_cong_qian.mp3"}
	];

	var loadingLayer = new LoadingSample1();
	addChild(loadingLayer);

	LLoadManage.load(
		loadData,
		function (p) {
			loadingLayer.setProgress(p);
		},
		function (r) {
			datalist = r;

			loadingLayer.remove();

			addBackgroundPic();
			addTitle();
			addAlbumPic();
			addMusic();
			addLyrics();
		}
	);
}

以上代码要做的就是首先使用LInit这个引擎内部函数初始化整个界面,并且进行全屏处理,然后通过静态类LLoadManage的load函数来加载图片和音乐,并将加载好的内容放到datalist中,然后调用addBackgroundPic,addTitle,addAlbumPic,addMusic,addLyrics这几个函数来添加显示对象和播放音乐。至于刚说到的LLoadManage和LInit等引擎中的用法可以参看lufylegend.js的API手册:

http://lufylegend.com/api/zh_CN/out/index.html

为了方便本地测试,我直接把歌词写到了shou_xie_de_cong_qian.js中。这么做可以避免使用ajax来读取文本文件而产生的本地运行时的问题。当然在实际应用中,歌词实质就是一个文本文件,是通过ajax等方式来读取这种文件,然后用我前面提到的类似的方法来解读文件。接下来先来看看shou_xie_de_cong_qian.js的内容:

var lyrics = "[ti:手写的从前][ar:周杰伦][t_time:(04:57)][00:03.23]周杰伦 - 手写的从前[00:06.56]词:方文山 曲:周杰伦[00:11.43]这风铃跟心动很接近 这封信还在怀念旅行[00:21.80]路过的爱情都太年轻 你是我想要再回去的风景[00:31.79]这别离被瓶装成秘密 这雏菊美的像诗句[00:39.30]而我在风中等你的消息 等月光落雪地[00:48.92]等枫红染秋季等相遇 我重温午后的阳光[00:58.24]将吉他斜背在肩上 跟多年前一样[01:05.71]我们轻轻的唱 去任何地方[01:14.54]我看着你的脸 轻刷著和弦[01:20.00]情人节卡片 手写的永远[01:24.05]还记得广场公园 一起表演[01:29.27]校园旁糖果店 记忆里在微甜…………";

图个方便,我就直接手动把歌词全部变成了一行。然后该到添加显示对象这一步了:

function addTitle () {
	var txt = new LTextField();
	txt.text = "手写的从前";
	txt.color = "white";
	txt.size = 25;
	txt.x = (LGlobal.width - txt.getWidth()) / 2;
	txt.y = 30;
	addChild(txt);
}

function addBackgroundPic () {
	var bmpd = new LBitmapData(datalist["pic"]);
	var bmp = new LBitmap(bmpd);
	bmp.scaleX = bmp.scaleY = 2;
	bmp.x = (LGlobal.width - bmp.getWidth()) / 2;
	bmp.y = (LGlobal.height - bmp.getHeight()) / 2;
	addChild(bmp);

	var curtain = new LSprite();
	curtain.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "black");
	curtain.alpha = 0.8;
	addChild(curtain);
}

function addAlbumPic () {
	var bmpd = new LBitmapData(datalist["pic"]);
	var bmp = new LBitmap(bmpd);
	bmp.scaleX = bmp.scaleY = 0.6;
	bmp.x = (LGlobal.width - bmp.getWidth()) / 2;
	bmp.y = 100;
	addChild(bmp);
}

function addMusic () {
	music = new LSound(datalist["music"]);
	music.play();
}

function addLyrics () {
	var lyricsLayer = new ytLyrics(music, getLyrics(lyrics));
	lyricsLayer.x = (LGlobal.width - lyricsLayer.getWidth()) / 2;
	lyricsLayer.y = 300;
	addChild(lyricsLayer);
}

这些函数中主要用到了lufylegend中的几个类:LTextField文本类,LBitmap和LBitmapData位图类,LSound音乐类,LSprite精灵类。具体用法请查阅API参考手册。在这里我们只谈addLyrics这个函数。这个函数中用到了我自己写的一个类ytLyrics,这个类派生自LSprite类,是个显示对象,作用就正如类名所示,是用来显示歌词的,有两个参数,第一个是当前播放的音乐,第二个是音乐的歌词。这个类留到过一会儿讲。

接下来是ytMain.js中解析歌词的函数:

function getLyrics (content) {
	var result = new Array();
	var cArr = content.split("[");
	cArr.shift();

	for (var i = 0; i < cArr.length; i++) {
		var o = cArr[i].split("]");

		if (o.length >= 2 && o[1] != "") {
			var tArr = o[0].split(":"), t = 0;

			if (tArr.length >= 2) {
				var mtArr = tArr[0].split(""), mt = 0;

				for (var k = 0; k < mtArr.length; k++) {
					if (Number(mtArr[k]) > 0) {
						mt += mtArr[k] * Math.pow(10, mtArr.length - k - 1);
					}
				}

				t += mt * 60;

				var stArr = tArr[1].split("."), intStArr = stArr[0].split(""), st = 0;

				for (var j = 0; j < intStArr.length; j++) {
					if (Number(intStArr[j]) > 0) {
						st += intStArr[j] * Math.pow(10, intStArr.length - j - 1);					
					}
				}

				t += Number(st + "." + stArr[1]);
			}

			result.push({time : t, content : o[1]});
		}
	}

	return result;
}

代码算不上长,但是本次研究到的精髓就在这里。在shou_xie_de_cong_qian.js中,我们已经定义了一个lyrics变量来装载歌词,因此我们目前传给这个函数的参数就是lyrics。在这里我主要用到了String的split来切割字符串,并循环由切割而得到的数组,将其继续解析下去,直到分出时间和歌词。由于歌词的格式不算太难,所以我就只分了两次就获取了时间和相应的歌词(第一次按“[”来分,第二次按“]”来分)。为了去除没有歌词的那三项,我通过判断第二次切割得到的数组中第二个元素是否为""来屏蔽掉没有歌词的那几项。大家可以仔细看一下代码,不难发现我把两次切割而得到的结果先放入JSON,然后再把JSON放到了result这个数组中,最后返回这个result数组。

也许你会发现我用到了t这个变量,这个变量也就是最后传到result中歌词对应的时间。为什么要另外弄一个t来显示时间呢?原因很简单,在HTML5 Audio中获取当前播放到的时间时,得到的是以秒作为单位,但是我们的歌词中时间的形式是[分:秒],所以我们就必须要把这种格式转换为以秒为单位的数字。实现方法就在上面提供的代码中。主要还是用split来切割这些时间字符串,然后得到以分为单位的数字,和以秒得到的数字。然后t = 以分得到的数字*60+以秒的数字,就能得到最后的结果了。

解析完歌词,再来看显示歌词的类ytLyrics,此物乃lufylegend中LSprite之后,得其父之显示属性,又另添己能,终可显示歌词也(译文:这个类是lufylegend引擎中LSprite的子类,有父类LSprite各项显示方面的属性和方法,再加上另外拓展的功能,最终就能显示歌词了):

function ytLyrics (music, lyricsList) {
	var s = this;
	LExtends(s, LSprite, []);

	s.index = 0;
	s.list = lyricsList;
	s.music = music;
	s.scrollY = 0;

	s.contentLayer = new LSprite();
	s.addChild(s.contentLayer);

	s.showContent();

	s.addEventListener(LEvent.ENTER_FRAME, s.loop);
}

ytLyrics.prototype.showContent = function () {
	var s = this;

	for (var i = 0; i < s.list.length; i++) {
		var txt = new LTextField();
		txt.text = s.list[i].content;
		txt.color = "white";
		txt.x = (LGlobal.width - txt.getWidth()) / 2;
		txt.y = s.contentLayer.getHeight() + 20;
		s.contentLayer.addChild(txt)
	}

	s.scrollY = 30;

	var maskLayer = new LSprite();
	maskLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, 200]);

	s.contentLayer.mask = maskLayer;

	s.contentLayer.filters = [new LDropShadowFilter()]
};

ytLyrics.prototype.loop = function (e) {
	var s = e.currentTarget;

	if (s.index >= s.list.length) {
		return;
	}

	var ct = s.music.getCurrentTime();

	var minT = s.list[s.index].time,
	maxT = ((s.index + 1) < s.list.length) ? s.list[s.index + 1].time : s.music.length;

	if (ct >= minT && ct <= maxT) {
		var preTxt = s.contentLayer.getChildAt(s.index - 1);

		if (preTxt) {
			preTxt.color = "white";

			LTweenLite.to(s.contentLayer, 1, {
				y : s.contentLayer.y - preTxt.getHeight() - 20
			});
		}

		var currentTxt = s.contentLayer.getChildAt(s.index);

		if (currentTxt) {
			currentTxt.color = "#33FF00";
		}

		s.index ++;
	}
};

代码也不长,主要用到了ENTER_FRAME时间轴事件来驱动滚动歌词。具体来说就是在时间轴时间中,不停地获取歌曲当前时间,然后在歌词列表中得到当前歌词对应的时间和下一条歌词的时间,比较歌曲的当前时间和当前歌词对应的时间、下一条歌词的时间,以此得到是否播放下一条歌词。显示方面还是用到了lufylegend,还是那句话,想要了解参考手册即可。

ok,运行代码就得到了咱们想要的效果。

三,源代码下载

上面讲了点精髓部分,其他的边角还需要大家自己来研究了,如果有任何不清除的地方,欢迎大家到本文下方留言。我会尽力回复各位的~

源代码下载地址:http://wyh.wjjsoft.com/downloads/lyrics.zip

本文到此就结束了,欢迎大家交流~

—————————————————————-

欢迎大家转载我的文章。

转载请注明:转自Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

分类: html5 标签:

HTML5物理游戏开发 – 越野山地自行车(二)创建一辆可操控的自行车

2014年4月13日 没有评论

第二章又拖到现在才发布,话说本次更新离上次已经很久了。不知道大家还记得上一章讲的内容否?

在上一章中,我们创建了各式各样的地形,今天我们就在这个地形之上,创建一辆自行车,并让它受到我们的控制。首先放上一张截图:

HTML5物理游戏开发 - 越野山地自行车(二)创建一辆可操控的自行车

没看过上一章的同学可以先移步到上一章。在了解上一章的内容之后,读起本章方可容易一些。

HTML5物理游戏开发 – 越野山地自行车(一)建立各式各样的地形

http://blog.csdn.net/yorhomwang/article/details/19710537

※再次声明,本次开发用到了lufylegend.js开源html5游戏引擎和box2dweb物理引擎,请到官方网站下载使用。官方网站地址已在第一章中说过了。

一,基础知识

首先我们来科普几个知识,只有了解这些知识之后,读起下面的内容才不会感觉头疼。

1,如何使刚体移动

在box2dweb中,要想使刚体移动,不能单纯地改变x、y坐标。其一,这样会使你的游戏失去了物理运动的效果;其二,在box2dweb中,直接调整刚体的位置是一个十分不好的方法,会违背物理运动原理,只有在刚体创建前的时候才用这个方法。所以在我们的自行车创建出来以后,想要移动刚体,最好的方法就是给刚体施加一个力。

在box2dweb中,施加力的方法有:ApplyForce、ApplyImpulse、SetLinearVelocity。本次使用的只有ApplyForce。其余的几种可以暂且不管。如果你确实想了解的话,可以看看ladeng6666的这篇文章:让刚体听我的——ApplyForce、ApplyImpulse、SetLinearVelocity

ApplyForce的用法如下:

ApplyForce(vec, pos)

这个函数的第个一个参数是给物体施加力的向量(b2Vec2);第二个pos是施加的位置,一般不确定位置的时候就取物体的重心(可以用b2Body的GetWorldCenter获取)。

2,如何构造像自行车这样形状复杂的物体

如何构造像自行车这样形状复杂的物体确实是一个值得思考的话题。对我们而言,第一个出现在脑子里的想法就是创建一个多边形刚体。当然这样实现起来极其麻烦,而且不仅仅是麻烦,做到最后更扫兴的是使用这种方法弄出来的刚体碰撞检测有问题,换言之,一个原本是凹凸多边形的刚体,突然就成了柔体,碰在其他物体上就会渗入其他物体里,so qipa~

那到底应该怎么办呢?我们不妨假设自己就真正地在制作一辆自行车。首先,我们要把材料和工具准备好。材料就是几个木块,工具就是一个锤子,一把铁钉。接下来,我们要做的就是用锤子铁钉把几个木块组装起来就ok了。这样看起来虽然so easy,但是有朋友会问,在box2dweb里到哪里去找锤子铁钉呢?锤子铁钉在box2dweb里倒是真没有,不过有一个更先进的东西——关节(joint)。

在这里主要就是需要两种关节:旋转关节(用于把轮子和支架绑起来),焊接关节(用于把各个支架固定起来)。

这两个关节在box2dweb里的使用方法依然不是那么简单,因此同样用到了lufylegend.js的封装。接下来就对这几个关节的用法进行说明。

■setRevoluteJoint(b2BodyA, b2BodyB, limits, motors)

b2BodyA:表示物体A (b2Body对象,可以用LSprite的box2dBody属性获取)
b2BodyB:表示物体B(b2Body对象,可以用LSprite的box2dBody属性获取)
limits:表示旋转角度限制数组,这个数组的内容是:[最小角度,最大角度],它在这里可以限制旋转关节旋转的角度(可以不传)
motors:表示马达数组,这个数组的内容是:[力度,速度],马达可以有很多用途,在这里,它可以是关节自动进行旋转(可以不传)

示例:

var backLayer,cLayer;
    function main(){    
    LGlobal.setDebug(true); 
    backLayer = new LSprite();  
    addChild(backLayer);    

    LGlobal.box2d = new LBox2d();
    cLayer = new LSprite();
    cLayer.x = 300;
    cLayer.y = 390;
    backLayer.addChild(cLayer);
    cLayer.addBodyPolygon(600,10,0,5,0.4,0.2);
    //加入一个动态的圆形物体1
    box01 = new LSprite();
    box01.x = 250;
    box01.y = 200;
    backLayer.addChild(box01);
    box01.addBodyCircle(100,0,0,1,1,0.4,0.2);
    box01.setBodyMouseJoint(true);
    //加入一个静态的圆形物体2
    box02 = new LSprite();
    box02.x = 250;
    box02.y = 150;
    backLayer.addChild(box02);
    box02.addBodyCircle(10,0,0,0,1,0.4,0.2);
    //加入一个旋转关节
    LGlobal.box2d.setRevoluteJoint(box01.box2dBody, box02.box2dBody ,[-360,720*5],[450,2]);
}

■setWeldJoint (b2BodyA, b2BodyB)

b2BodyA:表示捆绑对象物体A(b2Body对象,可以用LSprite的box2dBody属性获取)
b2BodyB:表示捆绑对象物体B(b2Body对象,可以用LSprite的box2dBody属性获取)

示例:

var backLayer,cLayer;
function main(){    
    LGlobal.setDebug(true); 
    backLayer = new LSprite();  
    addChild(backLayer);    
 
    LGlobal.box2d = new LBox2d();
    cLayer = new LSprite();
    cLayer.x = 300;
    cLayer.y = 390;
    backLayer.addChild(cLayer);
    cLayer.addBodyPolygon(600,10,0,5,0.4,0.2);
    //加入一个动态的圆形物体1
    box01 = new LSprite();
    box01.x = 200;
    box01.y = 100;
    backLayer.addChild(box01);
    box01.addBodyCircle(50,0,0,1,1,0.4,0.2);
    box01.setBodyMouseJoint(true);
    //加入一个动态的圆形物体2
    box02 = new LSprite();
    box02.x = 250;
    box02.y = 100;
    backLayer.addChild(box02);
    box02.addBodyCircle(50,0,0,1,1,0.4,0.2);
    box02.setBodyMouseJoint(true);
    //加入一个焊接关节
    LGlobal.box2d.setWeldJoint(box01.box2dBody, box02.box2dBody);
}

ok,基础知识差不多讲完了。进入正题吧。

二,修改Main类

上一章中的Main类大家还记得否?上次我没有加入真正的自行车,而是拿一个圆形小球在那里充当着,主要是给大家看看镜头跟随效果和各式各样的地形。这次既然要实现一个自行车,那么就要先把小球换掉,于是更改addBicycle函数,更新后如下:

Main.prototype.addBicycle = function(){
	var s = this;

	//创建自行车对象
	s.bicycleObj = new Bicycle(50,385);
	s.addChild(s.bicycleObj);
};

这里明显用到了Bicycle这个类。那么,我就立刻把笔尖指向Bicycle类吧,这个类是本章的重点内容,前面的基础讲解就是为这个类作铺垫的。

三,自行车类(Bicycle)

先看看构造器:

function Bicycle(sx,sy){
	var s = this;
	base(s,LSprite,[]);

	/**初始坐标*/
	s.sx = sx;
	s.sy = sy;

	/**刚体所属LSprite对象列表*/
	s.bodyList = new Array();

	/**添加左右移动力度向量*/
	s.moveVec = new LStage.box2d.b2Vec2();
	/**添加拉压操作力度向量*/
	s.tcVec = new LStage.box2d.b2Vec2();

	/**添加事件*/
	//键盘按下事件
	LEvent.addEventListener(window,LKeyboardEvent.KEY_DOWN,function(e){
		s.onKeydown(e,s);
	});
	//键盘松开事件
	LEvent.addEventListener(window,LKeyboardEvent.KEY_UP,function(e){
		s.onKeyup(e,s);
	});

	//初始化
	s.init();
}

构造器代码加入了详细的注释,所以我们直接进入讲解Bicycle的init函数。这是代码:

Bicycle.prototype.init = function(){
	var s = this;

	var sx = s.sx;
	var sy = s.sy; 

	/**轮子半径*/
	var wheelR = 20;
	/**轮子之间的距离*/
	var gapBetweenWheelAndWheel = 100;
	/**车手柄到轮子的距离*/
	var gapBetweenWheelAndHandlebar = 50;
	/**车把尺寸*/
	var handlebarWidth=20,handlebarHeight=5;
	/**座椅到轮子支架的距离*/
	var gapBetweenWheelFrameAndSeat = 30;
	/**座椅尺寸*/
	var seatWidth=30,seatHeight=5;
	/**支架尺寸*/
	var frameSize = 10;

	/**加入支架*/
	//轮子上的支架
	var frameAObj = new LSprite();
	frameAObj.x = sx+gapBetweenWheelAndWheel/2;
	frameAObj.y = sy+frameSize/2;
	frameAObj.addBodyPolygon(gapBetweenWheelAndWheel,frameSize,1,5);
	world.addChild(frameAObj);
	s.bodyList.push(frameAObj);
	//车把到轮子的支架
	var frameBObj = new LSprite();
	frameBObj.x = sx+gapBetweenWheelAndWheel-frameSize/2;
	frameBObj.y = sy-gapBetweenWheelAndHandlebar/2;
	frameBObj.addBodyPolygon(frameSize,gapBetweenWheelAndHandlebar,1,2);
	world.addChild(frameBObj);
	s.bodyList.push(frameBObj);

	/**加入车把*/
	var handlebarObj = new LSprite();
	handlebarObj.x = sx+gapBetweenWheelAndWheel-handlebarWidth/2-frameSize;
	handlebarObj.y = sy-gapBetweenWheelAndHandlebar+handlebarHeight/2;
	handlebarObj.addBodyPolygon(handlebarWidth,handlebarHeight,1,.5);
	world.addChild(handlebarObj);
	s.bodyList.push(handlebarObj);

	/**加入座椅*/
	//座椅到轮子支架的支架
	var seatFrameObj = new LSprite();
	seatFrameObj.x = sx+30;
	seatFrameObj.y = sy-gapBetweenWheelFrameAndSeat/2;
	seatFrameObj.addBodyPolygon(frameSize,gapBetweenWheelFrameAndSeat,1,1);
	world.addChild(seatFrameObj);
	s.bodyList.push(seatFrameObj);
	//座椅
	var seatObj = new LSprite();
	seatObj.x = sx+30;
	seatObj.y = sy-gapBetweenWheelFrameAndSeat-seatHeight/2;
	seatObj.addBodyPolygon(seatWidth,seatHeight,1,.5);
	world.addChild(seatObj);
	s.bodyList.push(seatObj);

	/**加入轮子*/
	//左边轮子A
	var wheelAObj = new LSprite();
	wheelAObj.x = sx-wheelR;
	wheelAObj.y = sy;
	wheelAObj.addBodyCircle(wheelR,wheelR,wheelR,1,2.5,.2,.4);
	world.addChild(wheelAObj);
	s.bodyList.push(wheelAObj);
	//右边轮子B
	var wheelBObj = new LSprite();
	wheelBObj.x = sx+gapBetweenWheelAndWheel-wheelR;
	wheelBObj.y = sy;
	wheelBObj.addBodyCircle(wheelR,wheelR,wheelR,1,2.5,.2,.4);
	world.addChild(wheelBObj);
	s.bodyList.push(wheelBObj);
	
	/**添加关节*/
	//轮子A和轮子支架的旋转关节
	LStage.box2d.setRevoluteJoint(frameAObj.box2dBody, wheelAObj.box2dBody);
	//轮子B和轮子支架的旋转关节
	LStage.box2d.setRevoluteJoint(frameAObj.box2dBody, wheelBObj.box2dBody);
	//车把到轮子的支架和轮子支架的焊接关节
	LStage.box2d.setWeldJoint(frameAObj.box2dBody, frameBObj.box2dBody);
	//车把到轮子的支架和车把的焊接关节
	LStage.box2d.setWeldJoint(handlebarObj.box2dBody, frameBObj.box2dBody);
	//轮子的支架和座椅的焊接关节
	LStage.box2d.setWeldJoint(seatFrameObj.box2dBody, frameAObj.box2dBody);
	//座椅的支架和座椅的焊接关节
	LStage.box2d.setWeldJoint(seatFrameObj.box2dBody, seatObj.box2dBody);
	
	/**遍历所有自行车零件刚体*/
	for(var key in s.bodyList){
		var obj = s.bodyList[key];
		//加入鼠标拖动
		if(obj.box2dBody)obj.setBodyMouseJoint(true);
		//设置对象名称
		obj.name = "bicycle";
	}

	/**设置主刚体*/
	s.mainBody = frameAObj.box2dBody;
	/**设置拉压操作刚体*/
	s.tcBody = wheelBObj.box2dBody;
};

这个函数看上去超长,但是逻辑却异常简单,大部分是重复的代码,再通过上面的基础讲解外加注释,看上去应该是无压力了吧,当然如果有不懂的,欢迎在本文下方留言。其中,我为了大家测试时的简化操作,给每一块刚体都设置了鼠标拖动,但是又由于刚体有很多,所以我把它们统统装在bodyList这个数组了,然后最后用遍历的方式给每一个刚体都设置了鼠标拖动。
最后来看看如何实现键盘操作。不难发现,在构造器里有这么一段代码:

/**添加事件*/
//键盘按下事件
LEvent.addEventListener(window,LKeyboardEvent.KEY_DOWN,function(e){
	s.onKeydown(e,s);
});
//键盘松开事件
LEvent.addEventListener(window,LKeyboardEvent.KEY_UP,function(e){
	s.onKeyup(e,s);
});

在段代码里,我们还用到了onKeydown和onKeyup这两个函数。代码如下:

Bicycle.prototype.onKeydown = function(e,s){
	var force = 50;
	switch(e.keyCode){
		//向右
		case 39:
			s.moveVec.x = force;
			break;
		//向左
		case 37:
			s.moveVec.x = -force;
			break;
		//向上
		case 38:
			s.tcVec.y = -force;
			break;
		//向下
		case 40:
			s.tcVec.y = force;
			break;
		default:
			return;
	}
	/**施加移动的力*/
	s.mainBody.ApplyForce(s.moveVec,s.mainBody.GetWorldCenter());
	/**施加拉压的力*/
	s.tcBody.ApplyForce(s.tcVec,s.tcBody.GetWorldCenter());
};
Bicycle.prototype.onKeyup = function(e,s){
	/**清空力度相量*/
	s.moveVec.SetZero();
	s.tcVec.SetZero();
};

在这里我们用到了之前讲过的ApplyForce。之前讲的时候,没有具体讲第一个参数是因为没有实际用到讲了等于徒劳。现在可以看到,这个b2Vec2有x,y属性,这两个属性可以控制力的方向。x<0向左,x>0向右,y>0向下,y<0向上。

当然,tcVec和moveVec是两个不同的向量,也就是说控制的不是同一个效果。moveVec是控制移动自行车的,这个不用多说。而tcVec是用于拉、压自行车的,我们知道,当在游戏中要开翻车的时候,往往可以用拉、压的方式来操控自行车,使其重新平衡,这个tcVec就是干这活儿的。

BTW,如果你不知道tcBody和mainBody是什么,可以看看Bicycle里init函数的代码。

添加完键盘事件后,我们就可以通过方向键来控制自行车了。操作方式为:上-拉,下-压,左-后退,右-前进。大家可以打开本文最下方的测试链接进行测试。

ok,至此,运行一下代码,得到的就是本文最上方图片所示的效果了。(什么?太丑了?你指的是我还是这个游戏?这个游戏啊,没贴图当然丑呢。)

奉上源代码下载地址:http://files.cnblogs.com/yorhom/box2dBicycle%282%29.rar

测试地址:http://www.cnblogs.com/yorhom/articles/box2dweb_bike2.html

下一章预告:目前,咋们的自行车是金刚不坏之身,你怎么摔它,它就是摔不碎(除非你把显示器抱起来,然后使劲往地上摔)。但是这不科学对吧……所以在下一章,我们就来看看,如何把这个自行车摔得支离破碎,体无完肤。哈哈,尽情期待~

本章就先到这里了。如果文章有任何疏漏之处,欢迎指正。当然,有不懂之处也欢迎各位在本文下方留言,我会尽力回复大家的。

—————————————————————-

欢迎大家转载我的文章。

转载请注明:转自Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

分类: html5 标签:

HTML5物理游戏开发 – 越野山地自行车(一)建立各式各样的地形

2014年3月2日 没有评论

在上一篇文章中,我们研究了一下Box2dWeb的锁链效果,当我研究出来以后,便突发奇想地想用可以这一效果制作一个越野自行车小游戏,让一个小自行车在各种地形之间来回颠簸。出于兴趣便对此研究了一番。今天就先来看看越野自行车里的地形是如何实现的。

一,准备工作

首先你需要下载lufylegend和box2dweb 这两个引擎。

box2dweb可以到这里下载:
http://code.google.com/p/box2dweb/downloads/list

lufylegend可以到这里:
http://lufylegend.com/lufylegend

关于lufylegend怎么用,可以到这里看看API文档:
http://lufylegend.com/lufylegend/api

Box2dWeb怎么用?其实我也不太清楚,这次主要用lufylegend封装的API,所以掌握lufylegend对box2dweb的操作就可以了。

在此之后,我们创建一个项目,就叫box2dBicycle吧,然后在里面分别建立data,lib,main文件夹和index.html。如下:

box2dBicycle

|—data

|—lib

|—main

|—index.html

接下来,我们来介绍一下这些文件夹是用来装什么的。data文件夹是用来装游戏中的关卡数据的,我准备把游戏中的关卡用json装起来,这些json就放在这个文件夹里。lib文件夹是装引擎用的,也就是box2dweb和lufylegend.js。main就是放我们源代码文件的。好了,介绍完了准备工作,我们来看看具体的代码。

二,初始游戏

既然是html5游戏,那么一定要有html代码,这些代码就在index.html中放着好了,代码如下:

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>box2d demo</title>
	<script type="text/javascript" src="./lib/Box2dWeb-2.1.a.3.min.js"></script>
	<script type="text/javascript" src="./lib/lufylegend-1.8.7.min.js"></script>
	
	<script type="text/javascript">
		init(50,"mylegend",800,450,gameInit);
		var world;
		var JS_FILE_PATH = "./main/";
		var LEVEL_FILE_PATH = "./data/"
		var loadData = [
			{path:JS_FILE_PATH+"Main.js",type:"js"},
			{path:JS_FILE_PATH+"Road.js",type:"js"},
			{path:JS_FILE_PATH+"BridgeGround.js",type:"js"},
			{path:JS_FILE_PATH+"SmoothGround.js",type:"js"},
			{path:JS_FILE_PATH+"HillGround.js",type:"js"},
			{path:JS_FILE_PATH+"SlopeGround.js",type:"js"},
			
			{path:LEVEL_FILE_PATH+"level01.js",type:"js"}
		];
		function gameInit(){
			LStage.setDebug(true);
			LStage.box2d = new LBox2d();

			if(LStage.canTouch == true){
				document.body.style.margin = "0px";
				document.body.style.padding = "0px";
				LStage.stageScale = LStageScaleMode.SHOW_ALL;
				LSystem.screen(LStage.FULL_SCREEN);
			}

			LLoadManage.load(loadData,null,function(){
				world = new Main();
				addChild(world);
				world.init();
			});
		}
	</script>
</head>
<body>
	<div id="mylegend"></div>
</body>
</html>

中间有一大段是js代码,这些代码就是游戏中的数据加载和lufylegend初始化,游戏全屏设置等。

接下来逐句解释一下这个js里的代码。首先是

init(50,"mylegend",800,450,gameInit);

也许你在纳闷这个init函数是啥,让我来告诉大家好了,这个函数是初始化lufylegend,也就是说是lufylegend中定义的。我这么写一行就说明要在id为"mylegend"的div中创建一个宽800,高450的canvas,游戏界面刷新次数为50ms一次,当界面初始完成后,调用gameInit函数。当然,具体用法还是看lufylegend.js api文档吧。

接着是一些变量:

var world;
var JS_FILE_PATH = "./main/";
var LEVEL_FILE_PATH = "./data/"
var loadData = [
	{path:JS_FILE_PATH+"Main.js",type:"js"},
	{path:JS_FILE_PATH+"Road.js",type:"js"},
	{path:JS_FILE_PATH+"BridgeGround.js",type:"js"},
	{path:JS_FILE_PATH+"SmoothGround.js",type:"js"},
	{path:JS_FILE_PATH+"HillGround.js",type:"js"},
	{path:JS_FILE_PATH+"SlopeGround.js",type:"js"},
	
	{path:LEVEL_FILE_PATH+"level01.js",type:"js"}
];

这些变量各有所用,打头的那个world是用于保存整个界面对象用的,现在暂时没有赋值,但在后面大家会看到他是一个Main类的实例,Main类是啥?在后面慢慢讲吧。然后JS_FILE_PATH和LEVEL_FILE_PATH是用来保存公共路径用的。loadData是装有需要加载资源的数组,这个数组里的内容写法有点特殊,不过是个lufylegend的固定写法,所以必须遵循库件规范。其中,每一条就是一个资源,可以是图片,可以是js文件,txt文件,mp3文件……在这里我用的是加载js,为什么不加载图片呢?因为没有用到图片呗……加载js文件时,需要设置path属性(也就是js文件路径),type属性(用于识别加载方式,加载js则填写为"js",txt就是"txt")。当然加载图片时的写法就不一样了,可以参考我以前写的文章,里面有加载图片时的资源列表的写法。

好了,这些变量解释完了,就来看gameInit部分了:

function gameInit(){
	LStage.setDebug(true);
	LStage.box2d = new LBox2d();

	if(LStage.canTouch == true){
		document.body.style.margin = "0px";
		document.body.style.padding = "0px";
		LStage.stageScale = LStageScaleMode.SHOW_ALL;
		LSystem.screen(LStage.FULL_SCREEN);
	}

	LLoadManage.load(loadData,null,function(){
		world = new Main();
		addChild(world);
		world.init();
	});
}

首先,因为我们的游戏中暂时没有图片,所以为了看到物理刚体我们就要设置debug模式,也就是通过LStage.setDebug(true)来实现。然后初始lufylegend中的box2d,所以用到了LStage.box2d = new LBox2d()。再来看看接下来的这段代码:

if(LStage.canTouch == true){
	document.body.style.margin = "0px";
	document.body.style.padding = "0px";
	LStage.stageScale = LStageScaleMode.SHOW_ALL;
	LSystem.screen(LStage.FULL_SCREEN);
}

这些代码有啥用呢?还是让我来告诉你吧,首先开头的if是为了限制平台用的,LStage这个静态类有一个canTouch属性,用来判断是否是移动端。如果是(true),那么就进入if中的代码,if中的代码是为了设置全屏用的,前两行是把body元素位置归0,然后通过LSystem.screen(LStage.FULL_SCREEN)来设置全屏,改LStage.stageScale属性是为了设置全屏模式,除了LStageScaleMode.SHOW_ALL之外,还有其他的几种,具体的还是看API文档吧。

最后到了加载资源这一步骤:

LLoadManage.load(loadData,null,function(){
	world = new Main();
	addChild(world);
	world.init();
});

LLoadManage这个类功能挺强大的,主要是可以将多个数据一起加载到游戏中(这里所说的“多个数据”就是上面我们写的那个名叫loadData的数组,也就是传给LLoadManage.load的第一个参数)。详细用法请移步API文档。这里只介绍最后一个参数,这个参数是一个function,这个function是在所有资源被加载完成之后调用的。所以说我们游戏一切的开始都应该是在这个函数中调用的,要不然加载的图片和js文件就使用不上了呢。当然在这里这个函数里的内容就是实例一个Main类出来,然后用addChild这个lufylegend中的方法把他加入到界面中,并调用init这个成员函数来初始化游戏,这个Main类会在下文作详细解释。关于addChild(obj),这个函数是用来添加显示对象到底层用的,也有对应的spriteObj.addChild(obj),也就是把obj添加到spriteObj里,这个spriteObj必须是一个LSprite,具体的解释还是看API文档吧,讲得绝对比我详细。

ok,游戏代码中的index.html讲完了。接下来切换主题,蹦到很基础的lufylegend添加刚体讲解。

三,基础讲解


1,什么是刚体

说实话,这个刚体是什么我也不是很清楚,我们不妨把它当成一个现实生活中的物体?刚体其实还有一个比较详细的解释,这个解释来自《HTML5 Canvas游戏开发实战》一书:刚体表示十分坚硬的物质,它上面任意两点的位置都是完全不变的,它就像“钻石”那样“坚硬”。

2,在lufylegend中,如何创建刚体

在lufylegend中,可以通过box2dweb创建圆形,方形,凸多边形(凹多边形也可以,但是box2dweb中对凹多边形碰撞处理有一些问题,所以不推荐使用)这几种刚体。

创建圆形刚体

var cLayer = new LSprite();
cLayer.x = 50 + Math.random()*700;
cLayer.y = 50;
addChild(cLayer);
//给LSprite加入圆形刚体
cLayer.addBodyCircle(50,50,50,1,0.5,0.4,0.5);

addBodyCircle(radius, cx, cy, type, density, friction, restitution)

■参数:
radius:半径
cx:圆心坐标x
cy:圆心坐标y
type:是否动态(1或0)
density:密度
friction:摩擦
restitution:弹性

创建方形刚体

cLayer = new LSprite();
cLayer.x = 50 + Math.random()*700;
cLayer.y = 50;
addChild(cLayer);
//给LSprite加入方形刚体
cLayer.addBodyPolygon(bitmap.getWidth(),bitmap.getHeight(),1,5,.4,.2);

addBodyPolygon(width, height, type, density, friction, restitution)

■参数:
width:矩形宽
height:矩形高
type:是否动态(1或0)
density:密度
friction:摩擦
restitution:弹性

创建多边形刚体

cLayer = new LSprite();
cLayer.x = 50 + Math.random()*700;
cLayer.y = 50;
addChild(cLayer);
//设置多边形顶点数组
var shapeArray = [
    [[0,54],[27,0],[54,54]]
];
//给LSprite添加多边形刚体
cLayer.addBodyVertices(shapeArray,27,27,1,.5,.4,.5);

addBodyVertices(v, cx, cy, type, density, friction, restitution)

■参数:
v:顶点数组
cx:中心坐标x
cy:中心坐标y
type:是否动态(1或0)
density:密度
friction:摩擦
restitution:弹性

ok,有了上面的介绍就可以进入下一个研究环节了。

四,建立各式各样的地形


1,Main类详解

首先来看看类的构造器。

function Main(){
	var s = this;
	base(s,LSprite,[]);

	/**设置场景大小*/
	s.sceneWidth = 8500;
	s.sceneHeight = LStage.height+1000;
}

打头的那句var s = this;可能就会懵住许多同学,疑问不在于var的使用,而在于为什么要这么写。其实在这里不这么写,把s换成this应该也可以,但是出于习惯,我还是加上了,原因在于如果以后在构造器里面出现了多个对象的使用,那么用this的指向就会出错。比如说我想在构造器里面加一个调用事件的函数,然后在这个调用的函数里面想访问类的一个属性,那么在这个事件监听函数中用this,指向的应该事件监听函数,而取不到类这个对象。这时候,用s把this代换掉,在事件监听函数中用s就可以来访问类了。第一行代码解释了这么久,相信熟悉js中class的朋友都看得不耐烦了吧。ok,咱们继续看下一行代码。

类里面第二行代码是base函数的运用。这个函数也是lufylegend里的全局函数,用于继承某个类。这里我选择继承了LSprite类。说实话,一般在lufylegend中用到继承,通常都是用的继承LSprite。继承了LSprite之后,我们就可以用到LSprite的一系列函数了,包括添加刚体的函数。

接下来的几行代码就是添加用于控制场景大小用的属性。这里显得它们很酱油,不过在后面还是用到了的。这两个属性顾名思义+注释,所以,不解释……(猛然看到LStage.height这个属性,这个是取canvas大小的属性,当然,还有LStage.width)

很简单的构造器代码就这样地解释完毕了。刚才我们说到Main有个init函数,看看吧?

Main.prototype.init = function(){
	var s = this;

	/**加入边框*/
	s.addBorder();
	/**加入路面*/
	s.addRoad();
	/**加入自行车*/
	s.addBicycle();
	/**加入循环事件*/
	s.addEventListener(LEvent.ENTER_FRAME,s.loop);
};

在这个函数里面,全部都是函数的调用。前三个是我自己拓展的,最后一个addEventListener是lufylegend中LSprite拥有的函数。(按理说应该是LInteractiveObject里才有addEventListener函数,但是LSprite继承自这个类,所以我才说到LSprite拥有这个函数)。这里的addEventListener和原始js中的不同,不过都是加事件用的,这个是仿照ActionScript设计的,参数依此是:[ 事件名称, 事件触发函数 ]。在这里触发的是loop这个成员函数,这个函数在现在没有任何作用,不过,过一阵子后,就必须要在里面做一些处理。

接下来我们来到addBorder这个函数:

Main.prototype.addBorder = function(){
	var s = this;

	/**创建边框*/
	//设置边框尺寸
	var borderSize = 10;
	//顶部边框
	var topBorder = new LSprite();
	topBorder.x = s.sceneWidth/2;
	topBorder.y = 5;
	topBorder.addBodyPolygon(s.sceneWidth,borderSize,0);
	s.addChild(topBorder);
	//右部边框
	var rightBorder = new LSprite();
	rightBorder.x = s.sceneWidth-5;
	rightBorder.y = s.sceneHeight/2;
	rightBorder.addBodyPolygon(borderSize,s.sceneHeight,0);
	s.addChild(rightBorder);
	//底部边框
	var bottomBorder = new LSprite();
	bottomBorder.x = s.sceneWidth/2;
	bottomBorder.y = s.sceneHeight-5;
	bottomBorder.addBodyPolygon(s.sceneWidth,borderSize,0);
	s.addChild(bottomBorder);
	//左部边框
	var leftBorder = new LSprite();
	leftBorder.x = 5;
	leftBorder.y = s.sceneHeight/2;
	leftBorder.addBodyPolygon(borderSize,s.sceneHeight,0);
	s.addChild(leftBorder);
};

从这里开始,我们就已经开始用到lufylegend加物理box2d刚体了。在上面的基础讲解中,我们都已经了解了这些,所以再加上注释,这些代码就不难看懂了。不过也许有朋友会问,加入这些边框是干什么用的?解释起来很简单,如果不加边框,当你加入动态的刚体时,刚体会落到显示范围之外,所以为了防止这一点发生,我们给这个世界加一个限制,把刚体“关”在里面。
然后是addRoad这个函数:

Main.prototype.addRoad = function(){
	var s = this;

	/**创建路面*/
	var roadObj = new Road(0,450);
	s.addChild(roadObj);
};

在这里面我们又用到了Road类,这个类一样是以后讲解,先把Main讲完再说吧。

然后是addBicycle这个函数

Main.prototype.addBicycle = function(){
	var s = this;

	//创建自行车对象
	s.bicycleObj = new LSprite();
	s.bicycleObj.x = 50;
	s.bicycleObj.y = 385;
	s.bicycleObj.addBodyCircle(30,30,30,1);
	s.bicycleObj.setBodyMouseJoint(true);
	s.addChild(s.bicycleObj);
};

由于暂时没有写关于自行车方面的代码,所以就先搞个圆形刚体给大家玩玩吧,当然本章重点不在自行车,在地形呢……上面的代码有一行没有介绍过,那就是setBodyMouseJoint这个函数。这个函数是让刚体可以进行鼠标拖拽用的。因为没有加入按键控制,所以就先来个鼠标操控。

最后到了loop函数。这个函数是在讲addEventListener时提到过的,忘记了的朋友不妨回忆一下那段代码。可以看到,我给的事件名称是LEvent.ENTER_FRAME,这个是哪里定义的呢?其实你知道了也没用,我是不会告诉你这个LEvent.ENTER_FRAME的值只是一个字符传而已。这个LEvent.ENTER_FRAME是lufylegend中的事件轴事件,时间轴事件是个啥?这个虽然问得很好,但是我在很多文章中都讲过,所以这里不讲……想了解更多的朋友不妨移步到《HTML5游戏引擎Lufylegend.js深入浅出》-【基础篇】-引擎介绍&原理 里面有详细的解释呢。回归正题,看看loop里的代码吧:

Main.prototype.loop = function(event){
	var s = event.target;
	var bo = s.bicycleObj;
	/**设置场景位置*/
	s.x = LStage.width*0.5 - (bo.x + bo.getWidth()*0.5);
	s.y = LStage.height*0.5 - (bo.y + bo.getHeight()*0.5);
	/**处理位置*/
	if(s.x > 0){
		s.x = 0;
	}else if(s.x < LStage.width - s.sceneWidth){
		s.x = LStage.width - s.sceneWidth;
	}
	if(s.y > 0){
		s.y = 0;
	}else if(s.y < LStage.height - s.sceneHeight){
		s.y = LStage.height - s.sceneHeight;
	}
	//计算刚体坐标
	LStage.box2d.synchronous();
};

首先我们通过时间轴事件触发时传给监听函数的event参数的target成员获取到自身这个对象(Main对象),然后,接下来的处理就是为了达到跟随镜头的效果。我们知道,在这种同类型的游戏中,场景通常都很大,如果不把镜头跟随自行车,那么,自行车开跑了,那就会直接跑到屏幕外去了,看不着了,所以在这里要处理一下。中间部分代码很简单,就是设置一些坐标罢了。只有最后一行LStage.box2d.synchronous()可能使大家有点懵。这个是lufylegend中用来重新计算刚体坐标用的。我们知道,把刚体加入到界面上后,刚体会进行物理运动,所以我们把刚体所在的LSprite改变了,刚体位置是会不变的,所以我们要把它们的位置重新计算一下好了。

好了,Main讲完了,该讲Road类了。

2,Road类

这个类顾名思义,就是用来创建各种地形的。先看构造器中的代码:

function Road(sx,sy){
	var s = this;
	base(s,LSprite,[]);

	//设置起始位置
	s.sx = sx;
	s.sy = sy;

	//设置新对象出现位置
	s.newObjPosX = s.sx;
	s.newObjPosY = s.sy;

	/**设置路面数据*/
	s.roadData = level01

	//初始化
	s.init();
}

同样是一些属性的设置,很简单。值得注意的是,roadData这个属性是赋值level01,这个前面所说的地形数据文件,也就是在data文件夹下的文件。我们先创建一个简单的地形,所以在level01.js中加入如下代码:

var level01 = [
	{
		type:Road.TYPE.Ground,
		groundWidth:200,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	},
	{
		type:Road.TYPE.Bridge,
		plankWidth:50,
		plankHeight:25,
		plankAmount:10
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:600,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	}
];

这个很明显是一个数组套json的数据格式,其中数组中每一条都是一个地形区,比如说Road.TYPE.Ground就是代表地面,然后地面又分地形,比如说Road.TERRAIN.Smooth代表平地。每一种不同的地形就会有不同的数据名称,比如说平地中有groundWidth和groundHeight,而吊桥(Road.TYPE.Bridge)中有plankWidth,plankHeight,plankAmount。至于其他的地形,我们以后再说吧。

回到Road类中,我们在上面的level01.js中看到了Road.XXX.xxx,这是在哪里定义的呢?看这里吧:

Road.TYPE = {
	//锁链桥
	Bridge:"bridge",
	//地面
	Ground:"ground",
	//空隙
	Spacing:"spacing"
};
Road.TERRAIN = {
	//平地
	Smooth:"smooth",
	//斜坡
	Slope:"slope",
	//山坡
	Hill:"hill"
};

我们暂时先定义这么多,以后有其他的地形再拓展吧。在Road类的构造器中调用过init这个函数,这个函数的代码如下:

Road.prototype.init = function(){
	var s = this;
	for(var key in s.roadData){
		var item = s.roadData[key];
		switch(item.type){
			/**路地*/
			case Road.TYPE.Ground:
				s.addGround(item);
				break;
			/**锁桥*/
			case Road.TYPE.Bridge:
				s.addBridge(item);
				break;
			/**空隙*/
			case Road.TYPE.Spacing:
				s.addSpacing(item);
				break;
		}
	}
};

在这个函数中,我们用到for和switch来加入不同的地形。接下来是addGround和addBridge以及addSpacing里的代码:

Road.prototype.addSpacing = function(data){
	var s = this;
	//设置新对象出现位置
	s.newObjPosX += data.spacingWidth;
	s.newObjPosY += data.spacingHeight;
};
Road.prototype.addBridge = function(data){
	var s = this;
	
	//加入锁链桥
	var bridgeGroundObj = new BridgeGround(
		s.newObjPosX,
		s.newObjPosY,
		data.plankAmount,
		data.plankWidth,
		data.plankHeight
	);
	world.addChild(bridgeGroundObj);
	//设置新对象出现位置
	s.newObjPosX += bridgeGroundObj.getGroundWidth();
};
Road.prototype.addGround = function(data){
	var s = this;
	
	switch(data.terrain){
		case Road.TERRAIN.Smooth:
			//加入平地路面
			var smoothGroundObj = new SmoothGround(
				s.newObjPosX,
				s.newObjPosY,
				data.groundWidth,
				data.groundHeight
			);
			world.addChild(smoothGroundObj);
			//设置新对象出现位置
			s.newObjPosX += smoothGroundObj.getGroundWidth();
			break;
		case Road.TERRAIN.Hill:
			//加入山坡路面
			var hillGroundObj = new HillGround(
				s.newObjPosX,
				s.newObjPosY,
				data.slopeAmount,
				data.hillWidth,
				data.hillHeight,
				data.groundWidth,
				data.groundHeight
			);
			world.addChild(hillGroundObj);
			//设置新对象出现位置
			s.newObjPosX += hillGroundObj.getGroundWidth();
			break;
		case Road.TERRAIN.Slope:
			//加入斜坡路面
			var slopeGroundObj = new SlopeGround(
				s.newObjPosX,
				s.newObjPosY,
				data.groundWidth,
				data.groundHeight,
				data.angle
			);
			world.addChild(slopeGroundObj);
			//设置新对象出现位置
			s.newObjPosX += slopeGroundObj.getGroundWidth();
			s.newObjPosY += slopeGroundObj.getGroundHeight();
			break;
	}
};

这看上去是一大串很长的代码,其实结构相似,主要是需要注意newObjPosX和newObjPosY这个属性的变换。当这个属性是用于确定下一个物体出现时的位置。在addSpacing时,没有其他的什么物体要加入,所以直接把newObjPosX和newObjPosY改变了就能达到效果。在addBridge中,出现这个类:BridgeGround。这个类就是在上一篇文章中所说的铁锁桥类。具体实现过程就看上一篇文章吧:【HTML5物理小Demo】用Box2dWeb实现锁链+弹簧效果

介绍完addSpacing和addBridge,我们来到了addGround函数,这个函数和上面两个有点区别,因为地面会分很多种地形,所以我们首先要用switch来区分到底添加哪个地形。我打算分成平地路面、斜坡路面、山坡路面。在平地路面中,我们用到了SmoothGround类,在斜坡路面用到了SlopeGround类,山坡路面用的是HillGround。接下来我们依此来看看这些类中的代码,先是SmoothGround:

function SmoothGround(sx,sy,w,h){
	var s = this;
	base(s,LSprite,[]);

	//保存路面尺寸
	s.groundW = w;
	s.groundH = h;
	//设置位置
	s.x = sx+w/2;
	s.y = sy;
	//初始化
	s.init();
}
SmoothGround.prototype.init = function(){
	var s = this;
	//加入刚体
	s.addBodyPolygon(s.groundW,s.groundH,0);
};
SmoothGround.prototype.getGroundWidth = function(){
	return this.groundW;
};
SmoothGround.prototype.getGroundHeight = function(){
	return this.groundH;
};

这个类是最简单的,所以就不加以分析了。接下来轮到SlopeGround类了:

function SlopeGround(sx,sy,w,h,angle){
	var s = this;
	base(s,LSprite,[]);

	//保存路面尺寸
	s.groundW = w;
	s.groundH = h;
	//保存坡度
	s.angle = angle;
	//设置位置
	s.x = sx+s.getGroundWidth()/2-(Math.sin(s.angle*Math.PI/180)*s.groundH*0.5);
	s.y = sy+s.getGroundHeight()/2;
	//初始化
	s.init();
}
SlopeGround.prototype.init = function(){
	var s = this;
	//加入刚体
	s.addBodyPolygon(s.groundW,s.groundH,0);
	//旋转刚体
	s.setRotate(s.angle*(Math.PI/180));
};
SlopeGround.prototype.getGroundWidth = function(){
	return (Math.cos(this.angle*Math.PI/180)*this.groundW);
};
SlopeGround.prototype.getGroundHeight = function(){
	return (Math.sin(this.angle*Math.PI/180)*this.groundW);
};

这个类和上一个类很类似,但是唯一不同的是我们在这个类中设置了刚体旋转,并且在取尺寸用到的函数getGroundHeight、getGroundWidth都用了sin和cos进行处理。设置刚体旋转用的是setRotate,参数是旋转的弧度(角度a转成弧度公式:a*Math.PI/180)。除了这几点之外,其他就和第一个讲的SmoothGround类没有什么区别了呢。

最后是HillGround,这个类很特殊,需要详细一点讲:

function HillGround(x,y,a,hw,hh,gw,gh){
	var s = this;
	base(s,LSprite,[]);

	//保存山坡尺寸
	s.hillW = hw;
	s.hillH = hh;
	//保存山坡平地部分尺寸
	s.groundW = gw;
	s.groundH = gh;
	//保存山坡数量
	s.hillAmount = a;
	//设置初始位置
	s.sx = x;
	s.sy = y;
	//初始化
	s.init();
}
HillGround.prototype.init = function(){
	var s = this;

	//加入山坡下方的平地
	var ground = new LSprite();
	ground.x = s.sx+s.getGroundWidth()/2;
	ground.y = s.sy;
	ground.addBodyPolygon(s.getGroundWidth(),s.groundH,0);
	world.addChild(ground);

	//设置山坡顶点初始位置
	var toX = 0;
	var toY = 0;
	//循环添加山坡
	for(var i=0; i<s.hillAmount; i++){
		//设置山坡顶点数组
		var shapeArray = new Array();
		shapeArray.push(new Array());
		shapeArray[0].push([toX,toY]);
		shapeArray[0].push(
			[toX+=s.hillW,toY-=s.hillH],
			[toX+=s.hillW,toY+=s.hillH]
		);
		var hill = new LSprite();
		hill.x = s.sx+s.groundW;
		hill.y = s.sy-s.hillH/2-s.groundH/2;
		//绘画多边形刚体
		hill.addBodyVertices(shapeArray,0,s.hillH/2,0);
		//加入显示列表
		world.addChild(hill);
	}
};
HillGround.prototype.getGroundWidth = function(){
	return this.groundW*2+this.hillAmount*2*this.hillW;
};
HillGround.prototype.getGroundHeight = function(){
	return this.groundH+this.hillH;
};

这个类最特别的是init函数部分。在这个函数中,我们为了添加小山丘,所以要画一个三角形刚体,实现三角形刚体可以用画多边形刚体用的函数addBodyVertices(),这个函数在上面的基础讲解部分提到过,参数需要一个顶点数组,所以我们在init中计算了每个顶点的坐标,然后把它们加入到顶点数组中。至于为什么要用for,那是因为考虑到会有连续的小山丘添加的情况。

好了,到了这一步算是成功了一大半。

最后拓展level01.js:

var level01 = [
	{
		type:Road.TYPE.Ground,
		groundWidth:200,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	},
	{
		type:Road.TYPE.Bridge,
		plankWidth:50,
		plankHeight:25,
		plankAmount:10
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:600,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:400,
		groundHeight:50,
		angle:20,
		terrain:Road.TERRAIN.Slope
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:400,
		groundHeight:50,
		angle:30,
		terrain:Road.TERRAIN.Slope
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:200,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	},
	{
		type:Road.TYPE.Ground,
		hillWidth:230,
		hillHeight:50,
		groundWidth:40,
		groundHeight:50,
		slopeAmount:2,
		terrain:Road.TERRAIN.Hill
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:300,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:400,
		groundHeight:50,
		angle:-30,
		terrain:Road.TERRAIN.Slope
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:200,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	},
	{
		type:Road.TYPE.Bridge,
		plankWidth:50,
		plankHeight:25,
		plankAmount:20
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:600,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	},
	{
		type:Road.TYPE.Spacing,
		spacingWidth:200,
		spacingHeight:150,
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:600,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	},
	{
		type:Road.TYPE.Ground,
		hillWidth:430,
		hillHeight:150,
		groundWidth:40,
		groundHeight:50,
		slopeAmount:1,
		terrain:Road.TERRAIN.Hill
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:800,
		groundHeight:50,
		angle:-30,
		terrain:Road.TERRAIN.Slope
	},
	{
		type:Road.TYPE.Ground,
		groundWidth:600,
		groundHeight:50,
		terrain:Road.TERRAIN.Smooth
	}
];

运行效果:
HTML5物理游戏开发 - 越野山地自行车(一)建立各式各样的地形

HTML5物理游戏开发 - 越野山地自行车(一)建立各式各样的地形

测试地址:http://www.cnblogs.com/yorhom/articles/box2dweb_bike1.html
打开测试地址后,用鼠标拖动圆球,使镜头跟随小球移动,看看我们创建的地形怎么样吧。


源代码下载:

http://files.cnblogs.com/yorhom/box2dBicycle%281%29.rar

本章就先到这里了。如果文章有任何疏漏之处,欢迎指正。当然,有不懂之处也欢迎各位在本文下方留言,我会尽力回复大家的。下一章。我们来研究一下如何用box2dweb+lufylegend创建一个自行车,并让这辆自行车能受到我们的控制,敬请期待~

—————————————————————-

欢迎大家转载我的文章。

转载请注明:转自Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

分类: html5 标签:

【HTML5物理小Demo】用Box2dWeb实现锁链+弹簧效果

2014年2月5日 没有评论

最近开始研究Box2dweb,Box2dweb是一款物理引擎,主要是对物理刚体和关节连接进行了封装,box2dweb很强大当然也有些复杂,不过幸好lufylegend.js做了这方面的封装,在制作时如果用lufylegend配合Box2dweb,那就简单多了。要学习box2dWeb我还是给大家推荐拉登大叔的博客,地址:http://www.ladeng6666.com/blog,写得相当好,话说他的文章中还运用了相当多的修辞手法呢,看他的文章边学技术,边学写作,哈哈。顺便也提一提,本次实现的效果也是模仿拉登大叔一篇文章中的效果,不过大叔的是ActionScript版本的,我是Js版的。

最后还是祝大家新年快乐吧~虽然这祝福来晚了,不过还是满含我的诚意……

好了,费话不多说,直接进入正题。

首先看截图吧,如下:

【HTML5物理小Demo】用Box2dWeb实现锁链+弹簧效果

【HTML5物理小Demo】用Box2dWeb实现锁链+弹簧效果

测试链接:http://game.h5stars.com/20141952ee70ee9c117/

接下来就来讲一讲实现步骤。

 

一,准备工作

首先你需要下载lufylegend和box2dweb 这两个引擎。

box2dweb可以到这里下载:

http://code.google.com/p/box2dweb/downloads/list

lufylegend可以到这里:

http://lufylegend.com/lufylegend

关于lufylegend怎么用,可以到这里看看API文档:

http://lufylegend.com/lufylegend/api

Box2dWeb怎么用?其实我也不太清楚,这次主要用lufylegend封装的API,用到原生API的时候我自己来讲讲吧,讲的差不要骂喔~

 

二,原理

这个小demo的原来其实很简单,就是将几块矩形小刚体用旋转关节连接起来。当然,说起来很容易,其实做起来还是要深究的。在我看到拉登大叔的文章之前,其实我也没有想到什么方法,于是就借鉴了大叔的文章,原理不是我独创的,所以呢,不好直说,免得大叔看到了不高兴,大家看一看拉登大叔的文章就能明白原理了。

文章地址如下:http://www.ladeng6666.com/blog/2012/11/25/create-box2d-linkage-or-bridge-effect-using-b2joint/

 

三,含详细注释的源代码

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>box2d demo</title>
	<script type="text/javascript" src="./Box2dWeb-2.1.a.3.min.js"></script>
	<script type="text/javascript" src="./lufylegend-1.8.7.min.js"></script>
	
	<script type="text/javascript">
		init(50,"mylegend",600,400,pageInit);
		function pageInit(){
			LStage.setDebug(true);
			LStage.box2d = new LBox2d();

			if(LStage.canTouch == true){
				document.body.style.margin = "0px 0px";
				LStage.stageScale = LStageScaleMode.SHOW_ALL;
				LSystem.screen(LStage.FULL_SCREEN);
			}

			var mainObj = new Main();
			addChild(mainObj);
		}

		/**
		 *Main
		 *@author: Yorhom
		 *@http://blog.csdn.net/yorhomwang
		*/
		function Main(){
			var s = this;
			base(s,LSprite,[]);

			/**加入围墙*/
			s.addWall();
			/**加入锁链桥*/
			s.addBridge();
			/**随机加入其他物体*/
			s.addRandomObj();
		}
		Main.prototype.addWall = function(){
			var s = this;
			
			//设置围墙大小
			var wallSize = 10;
			//设置围墙数据
			var wallList = [
				//左边
				[wallSize*0.5, LStage.height*0.5, wallSize, LStage.height],
				//右边
				[LStage.width-wallSize*0.5, LStage.height*0.5, wallSize, LStage.height],
				//上面
				[LStage.width*0.5, wallSize*0.5, LStage.width, wallSize],				
				//下面
				[LStage.width*0.5, LStage.height-wallSize*0.5, LStage.width, wallSize],
			];
			//通过遍历围墙数据,添加四面围墙
			for(var key in wallList){
				//获取数据
				var item = wallList[key];
				//创建围墙对象
				var wallLayer = new LSprite();
				//设定对象位置
				wallLayer.x = item[0];
				wallLayer.y = item[1];
				//加入刚体
				wallLayer.addBodyPolygon(item[2],item[3],0);
				//加入显示列表
				s.addChild(wallLayer);
			}
		};
		Main.prototype.addBridge = function(){
			var s = this;

			//关节向量
			var vec = new LStage.box2d.b2Vec2();
			//添加对象数量
			var amount = 6;
			//设置对象宽度和高度
			var bw=50,bh=15;
			//获取锁链桥总长度
			var bridgeWidth = bw*amount;
			//设置锁链桥开始位置
			var initX=(LStage.width-bridgeWidth)*0.5,
				initY=(LStage.height-bh)*0.5+30;
			//设置用于固定锁链桥的定点半径
			var anchorR = 15;

			/**用于固定锁链桥的定点A*/
			var anchorA = new LSprite();
			anchorA.x = initX-anchorR;
			anchorA.y = initY-anchorR;
			anchorA.addBodyCircle(anchorR,anchorR,anchorR,0);
			s.addChild(anchorA);
			/**用于固定锁链桥的定点B*/
			var anchorB = new LSprite();
			anchorB.x = bridgeWidth+initX-anchorR;
			anchorB.y = initY-anchorR;
			anchorB.addBodyCircle(anchorR,anchorR,anchorR,0);
			s.addChild(anchorB);

			//上一个对象
			var previousBlock = anchorA;

			/**循环添加刚体*/
			for(var i=0; i<amount; i++){
				//实例化对象
				var block = new LSprite();
				//设定对象位置
				block.x = initX+i*bw+bw*0.5;
				block.y = initY;
				//加入刚体
				block.addBodyPolygon(bw,bh,1);
				//设置鼠标拖动
				block.setBodyMouseJoint(true);
				//加入显示列表
				s.addChild(block);
				//加入关节
				var revoluteJoint = new LStage.box2d.b2RevoluteJointDef();
				vec.Set((initX+i*bw)/30, initY/30);
				revoluteJoint.Initialize(previousBlock.box2dBody,block.box2dBody,vec);
				LStage.box2d.world.CreateJoint(revoluteJoint);
				//更改上一个对象
				previousBlock = block;
			}
			//将最后一个刚体固定
			var revoluteJoint = new LStage.box2d.b2RevoluteJointDef();
			vec.Set((initX+i*bw)/30, initY/30);
			revoluteJoint.Initialize(previousBlock.box2dBody,anchorB.box2dBody,vec);
			LStage.box2d.world.CreateJoint(revoluteJoint);
		};
		Main.prototype.addRandomObj = function(){
			var s = this;
			for(var i=0; i<10; i++){
				//创建对象
				var obj = new LSprite();
				//设置对象位置
				obj.x = Math.floor(Math.random()*(400-200+1)+200);
				obj.y = 0;
				//加入显示列表
				s.addChild(obj);
				//根据随机数添加不同的刚体
				if(Math.random() > 0.5){
					//获取随机宽度和高度
					var w = Math.floor(Math.random()*10)+25;
					var h = Math.floor(Math.random()*10)+25;
					//重现设置y坐标
					obj.y += h*0.5;
					//添加矩形刚体
					obj.addBodyPolygon(w,h,1);
				}else{
					//获取随机半径
					var r = Math.floor(Math.random()*20)+5;
					//重现设置y坐标
					obj.y += r;
					//添加圆形刚体
					obj.addBodyCircle(r,r,r,1);
				}
				//设置鼠标拖动
				obj.setBodyMouseJoint(true);
			}
		};
	</script>
</head>
<body>
	<div id="mylegend"></div>
</body>
</html>

所有代码都在这里了,还是很少的,对吧。看了拉登大叔的原理讲解,再来看我写的js代码就非常容易了。主要是注意以下几个地方。

1,添加刚体

在box2dweb中添加刚体超级麻烦,把整个创建过程告诉大家估计大家都会觉得不耐烦,所以,我用到了lufylegend.js添加刚体,这样一来添加刚体就被简化成一步了。非常方便,不是吗?在lufylegend中添加刚体一共有这几个函数:

addBodyCircle() 添加原形刚体
addBodyPolygon() 添加矩形刚体
addBodyVertices() 添加不规则图形刚体

具体的参数说明和使用举例可以参见lufylegend的API文档中LSprite的API。文档地址已在文章的准备工作一栏写出。

2,添加关节

在lufylegend虽然也有添加关节的封装,但是不能设置关节点。所以我还是用了box2dweb原生方法。添加关节的代码如下:

var revoluteJoint = new LStage.box2d.b2RevoluteJointDef();
vec.Set((initX+i*bw)/30, initY/30);
revoluteJoint.Initialize(previousBlock.box2dBody,block.box2dBody,vec);
LStage.box2d.world.CreateJoint(revoluteJoint);

其他的好理解,主要是vec这个变量,它在这里是一个向量对象,实例化代码如下:

var vec = new LStage.box2d.b2Vec2();

其实这个b2Vec2这个类可以传参数,和它的成员函数Set的参数是一样的。但是这里又多个关节,所以就没有直接设置,而是在后面用Set方法设置。这个向量可以看成一个json吧,传的参数就是设定关节的位置(x,y)。

在创建了关节之后,千万别忘记下面的代码:

LStage.box2d.world.CreateJoint(revoluteJoint);

另外,Initialize方法传的参数是box2dweb的刚体对象,在LSprite中的 box2dBody就可以获取该LSprite添加的刚体。

3,固定动态刚体

在box2dweb中,刚体分动态和静态。静态刚体比较老实,就呆在原地不动;动态刚体很活泼,不停地在做运动。

拉登大叔其实也写过相关的文章,但是我没怎么看,于是就自己想了一个方法:首先在锁链的两端建立两个圆形静态刚体,然后把锁链两端的两块矩形刚体分别连在附近的圆形静态刚体上,圆形静态刚体就会帮忙把锁链拽着,不让它乱跑。但由于锁链中每个矩形刚体是动态的,所以他们还是可以互相牵扯的。

4,如果你自己在用lufylegend+box2dweb编写时看不到图象,该怎么办?

首先你要检测一下是否在最底层上加了一个LSprite,并且用这个LSprite的graphics画了一个不透明的背景。如果是的话,你可以把这个LSprite去掉,然后再看看有没有。如果还没有,可能有以下几种原因:(1)没有在用之前加入LStage.setDebug(true); (2)没有在使用box2dweb之前加入LStage.box2d = new LBox2d();

 

源代码在上面已经全部给出了,运行时需要配置一下引擎box2dweb和lufylegend,然后把引擎的压缩版本(文件名含.min)放在与html同级的文件目录下即可运行,测试愉快~

分类: box2D, html5 标签:

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&C++版)

2014年1月13日 没有评论

这几天放寒假了,时间也多了起来,当然又有时间搞搞程序了。哈哈~

昨天在开发我的塔防游戏时突然发现人物实际攻击范围比规定的范围小,按理说应该是一样大的,但偏偏不是,我被这个问题搞得糊里糊涂的,一直没想出问题所在。最后询问了一个程序高手——我哥哥。他虽然是搞C++的,但听了我代码解释中有检测圆形碰撞时,他立刻就发现了问题,他告诉我,敌人可以看作是方块,而攻击范围是圆的,如果把敌人弄成圆形进行碰撞检测那必然不准,应该检测矩形和圆形碰撞才行。我听了之后恍然大悟,但是lufylegend中没有这个功能,怎么办呢?我第一想法是对lufy说说,让他老人家实现吧。当我点开Google Talk准备发起对话时,突然又想到一来要是lufy老人家去实现,那要等到猴年马月去了,况且lufy前辈琐事缠身,要是我总是给他提意见不帮忙解决,他老人家是不是想打我啊……最后我还是决定自己来实现吧。但是我没搞过这方面的研究,没有经验,所以一开始有点懵,于是就去Google了一下,发现还真有人讲过,于是就点开看了,不知道是我理解能力不好还是文章写得差(估计都是我理解能力不好……)我看了半晌没看懂,呜呼,无法可想,我当时就失望了。但是文章下面有段C代码,于是我把它移植到js上来,运行了一下,感觉效果还不错,于是就马上跑到github上把代码上传给lufy了,并做了一个demo,并在Google Talk上提醒了lufy叫他老人家测试,结果lufy拿到代码一测试就发现了bug。我X,不愧是大神啊……没有办法,只有另谋出路了。

今早起来,哎呀,天气不错啊,成都好久没有这么爽的天气了。只见空气清新,阳光和煦,真是外出骑车的好机会啊!于是我便和家人一块儿跑到外面溜达了一圈。边走边想矩形和圆形碰撞的事。半天想不明白,呜呼,我只好在路上问了问哥哥。哥哥果然是高手,他想了一会儿便说出了重点,给了我启发。于是回到家,我便马上打开电脑,进行了实验,结果还成功了。当然,按照以往我的习惯,这次小小“发明”也当然也要分享给大家啦~

(以上事情均发生在1月11日和1月12日,所以lufy看到文章开头不要以为走错家门了……)

上面说到了我的塔防游戏,现在已经完工得差不多了~大家可以看看它的一些介绍:http://www.cnblogs.com/yorhom/p/sanguotd.html ,顺便发几张截图,给大伙提提兴趣。

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)

废话写了一大堆,接下来还是来看看矩形和圆形碰撞实现过程吧。

一,原理介绍

这回有点复杂,不过看懂了还是很好理解的。当然,我不敢保证这种算法在任何情况下都会起效果,如果有同学测试时,发现出现错误,请及时联系我。

我们首先来建立一个以圆心为原点的坐标系:

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)

然后要检测碰撞就只有两种情况了。

情况一,矩形全部都在一个象限内,如图:

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)

当然,图中只是举个例子,不一定是只在第二象限,任何一个象限都行,只要是矩形全在该象限。

这种情况比较好解决,首先,我们计算出矩形每个角的坐标,然后用勾股定律依次算出这个角到圆心的距离是否小于或者等于半径。设这个角与圆心横坐标之差为d1,纵坐标之差为d2,半径为r,公式表达如下:

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)

如果有一个角满足要求说明产生碰撞,返回true。

但是有朋友懵了,怎么判断矩形是不是在一个象限内呢?很简单,只要判断这个矩形左上角和右下角是否在同一个象限内就可以了。于是我们得写个函数来实现判断某两个角是否在同一象限。

函数代码如下:

function isSameQuadrant(cood,objA,objB){
	var coodX = cood.x;
	var coodY = cood.y;
	var xoA = objA.x
	,yoA = objA.y
	,xoB = objB.x
	,yoB = objB.y;
	
	if(xoA-coodX>0 && xoB-coodX>0){
		if((yoA-coodY>0 && yoB-coodY>0) || (yoA-coodY<0 && yoB-coodY<0)){
			return true;
		}
		return false;
	}else if(xoA-coodX<0 && xoB-coodX<0){
		if((yoA-coodY>0 && yoB-coodY>0) || (yoA-coodY<0 && yoB-coodY<0)){
			return true;
		}
		return false;
	}else{
		return false;
	}
}

这个函数原本是准备写到lufylegend中LMath静态类中的,参数原本是LPoint对象,但是这里可以用json,因为LPoint里的x,y属性可以写到json里,函数也就同样取得出值了。函数参数介绍:[cood创建的坐标系原点坐标, objA第一个点坐标, objB第二个点坐标] 这几个参数均为json对象,格式为:

{x:点的x坐标, y:点的y坐标}

函数中的代码还是很好理解的,就是判断一下两个点的x坐标都分别减去原点x坐标,看得出的数正负符号是否相同,然后又用同样的办法算出y轴上的符号是否相同,如果都相同就在同一象限。

有了这个函数,剩下得就好办了,直接代入开头给出的公式进行计算即可。

情况二,矩形跨度两个象限或者两个象限以上

这种情况更好办,我们就可以直接把圆看作一个边长为2r正方形,然后用矩形碰撞算法检测正方形和矩形的碰撞,如下图所示:

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)

矩形碰撞的算法是什么呢?很easy,如图:

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)

如果要横向判断碰撞的话,判断(x1-x2)的绝对值是否小于或者等于w1/2+w2/2,如果是则横向则有碰撞。纵向判断是一样的,判断(y1-y2)的绝对值是否小于或等于h1/2+h2/2即可。

有了这些算法,我们就可以实现情况2了。

二,Javascript版算法&测试代码

先上代码吧:

function hitTestRectArc(rectObj,arcObj,rectVec,arcR){
	var rw = rectObj.getWidth()
	,rh = rectObj.getHeight()
	,ar = arcObj.getWidth()*0.5
	,rx = rectObj.x
	,ry = rectObj.y
	,ax = arcObj.x
	,ay = arcObj.y;
	
	if(typeof rectVec != UNDEFINED){
		rx += (rw - rectVec[0])*0.5;
		ry += (rh - rectVec[1])*0.5;
		rw = rectVec[0];
		rh = rectVec[1];
	}
	if(typeof arcR != UNDEFINED){
		ax += (ar - arcR);
		ay += (ar - arcR);
		ar = arcR;
	}
	
	var rcx = rx+rw*0.5,rcy = ry+rh*0.5;
	var rltx = rx
	,rlty = ry
	,rlbx = rx
	,rlby = ry+rh
	,rrtx = rx+rw
	,rrty = ry
	,rrbx = rx+rw
	,rrby = ry+rh;
	
	if(
		isSameQuadrant(
			{x:ax,y:ay},
			{x:rltx,y:rlty},
			{x:rrbx,y:rrby}
		)
	){
		var dX1 = Math.abs(ax-rltx),dY1 = Math.abs(ay-rlty);
		var dX2 = Math.abs(ax-rlbx),dY2 = Math.abs(ay-rlby);
		var dX3 = Math.abs(ax-rrtx),dY3 = Math.abs(ay-rrty);
		var dX4 = Math.abs(ax-rrbx),dY4 = Math.abs(ay-rrby);
		
		if(
			(((dX1*dX1) + (dY1*dY1)) <= (ar*ar))
			||(((dX2*dX2) + (dY2*dY2)) <= (ar*ar))
			||(((dX3*dX3) + (dY3*dY3)) <= (ar*ar))
			||(((dX4*dX4) + (dY4*dY4)) <= (ar*ar))
		){
			return true;
		}
		return false;
	}else{
		var result = false;
		var squareX = ax
		,squareY = ay
		,squareW = ar*2
		,squareH = squareW;
		if(
			(Math.abs(squareX-rcx) <= (squareW+rw)*0.5)
			&&(Math.abs(squareY-rcy) <= (squareH+rh)*0.5)
		){
			result = true;
		}
		return result;
	}
}

由于是为lufylegend设计的函数,所以参数为 [ rectObj矩形对象(LSprite或者LShape对象), arcObj圆形对象(LSprite或者LShape对象), rectVec矩形规定大小(可不填), arcR圆形半径(可不填)] 当然,或许些朋友不懂这几行代码:

var rw = rectObj.getWidth()
,rh = rectObj.getHeight()
,ar = arcObj.getWidth()*0.5
,rx = rectObj.x
,ry = rectObj.y
,ax = arcObj.x
,ay = arcObj.y;

好吧,我告诉你,这里用到的是lufylegend中LSprite和LShape,这两个类有x、y属性,还有获取宽度和高度的getWidth()和getHeight(),这里看不懂没关系,你知道是取高度和宽度还有x,y坐标的就行了。当然你要深究,那就看看lufylegend.js的API文档吧:http://lufylegend.com/lufylegend/api ,以下测试代码也用到了lufylegend.js,据说这个引擎是个不错的引擎,想了解的同学,去官方网站看看吧:http://lufylegend.com/lufylegend/ 或者看看我的文章,大多数是讲解有关lufylegend开发的。

示例代码:

init(50,"mylegend",500,250,main);

function main(){
	LGlobal.setDebug(true);
				
	var back = new LSprite();
	back.graphics.drawRect(5,"green",[0,0,LStage.width,LStage.height],true,"lightblue");
	addChild(back);
	
	var cObj = new LSprite();
	cObj.x = 200;
	cObj.y = 120;
	cObj.graphics.drawArc(0,"",[0,0,50,0,2*Math.PI],true,"red");
	back.addChild(cObj);
	
	var rObj = new LSprite();
	rObj.x = 250;
	rObj.y = 70;
	rObj.alpha = 0.8;
	rObj.graphics.drawRect(0,"",[0,0,100,100],true,"green");
	back.addChild(rObj);

	trace(hitTestRectArc(rObj,cObj));
	
	back.addEventListener(LMouseEvent.MOUSE_DOWN,function(e){
		rObj.x = e.offsetX-rObj.getWidth()*0.5;
		rObj.y = e.offsetY-rObj.getHeight()*0.5;
		trace(hitTestRectArc(rObj,cObj));
	});
}

测试链接:http://www.cnblogs.com/yorhom/articles/hitTestRectArc.html

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)

三,C++版

C++版我用的是Qt,所以大家运行要在Qt creator里编译运行。

HitTestAlg.h里的代码:

#ifndef HITTESTALG_H
#define HITTESTALG_H

#include <math.h>
#include <QPoint>
#include <QRect>

class CMath
{

public:

	static int pow(int base, int powerOf)
	{
		return (int)::pow((double)base, (double)powerOf);
	}

	static int sqrt(int n)
	{
		return (int)::sqrt((double)n);
	}

	static int abs(int n)
	{
		n = n < 0 ? -n : n;
		return n;
	}

	static int distance(const QPoint& pt1, const QPoint& pt2)
	{
		return CMath::sqrt(CMath::pow(CMath::abs(pt1.x() - pt2.x()), 2) + CMath::pow(CMath::abs(pt1.y() - pt2.y()), 2));
	}

};

class CArc
{

protected:

	int	m_nRadius;
	QPoint	m_ptCenter;

public:

	CArc() : m_nRadius(0), m_ptCenter(0, 0){}
	CArc(const CArc& arc) : m_nRadius(arc.radius()), m_ptCenter(arc.center()){}
	CArc(int radius, QPoint center) : m_nRadius(radius), m_ptCenter(center){}
	CArc(int radius, int centerX, int centerY) : m_nRadius(radius), m_ptCenter(centerX, centerY){}
	~CArc(){}

	void setRadius(int radius){m_nRadius = radius;}
	int radius() const {return m_nRadius;}
	void setCenter(const QPoint& center){m_ptCenter = center;}
	void setCenter(int centerX, int centerY){m_ptCenter = QPoint(centerX, centerY);}
	QPoint center() const {return m_ptCenter;}
	QRect rect() const {return QRect(center().x() - radius(), center().y() - radius(), 2 * radius(), 2 * radius());}

};

class CHitTestAlg
{

protected:

	QRect	m_rtRect;
	CArc	m_arArc;

protected:

	bool locatedSameQuadrant() const
	{
		bool bRes = false;
		int nRectLeft = m_rtRect.left(), nRectTop = m_rtRect.top(), nRectRight = m_rtRect.right(), nRectBottom = m_rtRect.bottom();
		int nArcCenterX = m_arArc.center().x(), nArcCenterY = m_arArc.center().y();
		if((nRectLeft - nArcCenterX >= 0 && nRectRight - nArcCenterX >= 0 && nRectTop - nArcCenterY <= 0 && nRectBottom - nArcCenterY <= 0)
			|| (nRectLeft - nArcCenterX <= 0 && nRectRight - nArcCenterX <= 0 && nRectTop - nArcCenterY <= 0 && nRectBottom - nArcCenterY <= 0)
			|| (nRectLeft - nArcCenterX <= 0 && nRectRight - nArcCenterX <= 0 && nRectTop - nArcCenterY >= 0 && nRectBottom - nArcCenterY >= 0)
			|| (nRectLeft - nArcCenterX >= 0 && nRectRight - nArcCenterX >= 0 && nRectTop - nArcCenterY >= 0 && nRectBottom - nArcCenterY >= 0)
		){
			bRes = true;
		}
		return bRes;
	}

	bool hitTestRect() const
	{
		QRect rtArc = m_arArc.rect();
		bool bRes = false;
		if(CMath::abs(m_rtRect.center().x() - rtArc.center().x()) <= CMath::abs((m_rtRect.width() + rtArc.width()) / 2)
			&& CMath::abs(m_rtRect.center().y() - rtArc.center().y()) <= CMath::abs((m_rtRect.height() + rtArc.height()) / 2)
		){
			bRes = true;
		}
		return bRes;
	}

	bool hitTestAngleArc() const
	{
		bool bRes = false;
		QPoint ptRectTopLeft = m_rtRect.topLeft(), ptRectTopRight = m_rtRect.topRight()
		, ptRectBottomLeft = m_rtRect.bottomLeft(), ptRectBottomRight = m_rtRect.bottomRight()
		, ptArcCenter = m_arArc.center();
		int nArcRadius = m_arArc.radius();

		if(CMath::distance(ptRectTopLeft, ptArcCenter) <= nArcRadius
			|| CMath::distance(ptRectTopRight, ptArcCenter) <= nArcRadius
			|| CMath::distance(ptRectBottomLeft, ptArcCenter) <= nArcRadius
			|| CMath::distance(ptRectBottomRight, ptArcCenter) <= nArcRadius
		){
			bRes = true;
		}
		return bRes;
	}

public:

	CHitTestAlg(const QRect& rect, const CArc& arc) : m_rtRect(rect), m_arArc(arc){}
	~CHitTestAlg(){}

	bool hitTest() const
	{
		bool bRes = false;
		if(locatedSameQuadrant()){
			bRes = hitTestAngleArc();
		}else{
			bRes = hitTestRect();
		}
		return bRes;
	}

};

#endif // HITTESTALG_H

mainwindow.h里的代码:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include "HitTestAlg.h"

class MainWindow : public QWidget
{
	Q_OBJECT

protected:

	QRect		m_rtRect;
	CArc		m_arArc;
	bool		m_bHit;

protected:

	virtual void mouseReleaseEvent(QMouseEvent *mouseEvent);
	virtual void paintEvent(QPaintEvent *paintEvent);

public:

	MainWindow(QWidget *parent = 0);
	~MainWindow();

};

#endif // MAINWINDOW_H

mainwindow.cpp里的代码:

#include <QDebug>
#include <QMouseEvent>
#include <QBrush>
#include <QPainter>
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
	: QWidget(parent)
	, m_rtRect(0, 0, 100, 50)
	, m_arArc(50, 200, 200)
	, m_bHit(false)
{
	QWidget::showMaximized();
}

MainWindow::~MainWindow()
{
	
}

void MainWindow::mouseReleaseEvent(QMouseEvent *mouseEvent)
{
	if(mouseEvent){
		QPoint ptPos = mouseEvent->pos();
		QRect rtRect;
		rtRect.setX(ptPos.x() - m_rtRect.width() / 2);
		rtRect.setY(ptPos.y() - m_rtRect.height() / 2);
		rtRect.setWidth(m_rtRect.width());
		rtRect.setHeight(m_rtRect.height());
		m_rtRect = rtRect;
		m_bHit = CHitTestAlg(m_rtRect, m_arArc).hitTest();
		QWidget::update();
	}
}

void MainWindow::paintEvent(QPaintEvent *paintEvent)
{
	Q_UNUSED(paintEvent)

	QPainter xPainter(this);
	{
	xPainter.save();
	QBrush xBrush; xBrush.setColor(Qt::red); xBrush.setStyle(Qt::SolidPattern);
	QPen xPen; xPen.setColor(Qt::black); xPen.setStyle(Qt::SolidLine);
	xPainter.setBrush(xBrush);
	xPainter.setPen(xPen);
	xPainter.drawEllipse(m_arArc.center(), m_arArc.radius(), m_arArc.radius());
	xPainter.restore();
	}
	{
	xPainter.save();
	QBrush xBrush; xBrush.setColor(Qt::darkGreen); xBrush.setStyle(Qt::SolidPattern);
	QPen xPen; xPen.setColor(Qt::black); xPen.setStyle(Qt::SolidLine);
	xPainter.setBrush(xBrush);
	xPainter.setPen(xPen);
	xPainter.drawRect(m_rtRect);
	xPainter.restore();
	}
	{
	xPainter.save();
	QString sContent = QString("Hit Test: %1").arg(m_bHit ? "true" : "false");
	QFont ftFont("Tahoma", 12, QFont::DemiBold, true);
	xPainter.setFont(ftFont);
	xPainter.drawText(20, m_arArc.rect().bottom() + 30, sContent);
	xPainter.restore();
	}
}

main.cpp里的代码:

#include <QApplication>
#include "mainwindow.h"

int main(int argc, char *argv[])
{
	QApplication a(argc, argv);
	MainWindow w;
	w.show();
	
	return a.exec();
}

原理和js版是一样的,就不多解释了,在下面我会放出所有代码。C++版运行demo如下:

2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascript&amp;C++版)
Qt做的界面感觉还是不错的,哈哈~~

源代码下载:http://files.cnblogs.com/yorhom/hitTestRectArc.rar

本文就到此为止,以上就是本篇所有内容,欢迎大家交流。

—————————————————————-

欢迎大家转载我的文章。

转载请注明:转自Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

分类: html5 标签:

HTML5游戏引擎lufylegend深入浅出 – 引擎介绍&原理

2013年12月14日 没有评论

又很久没有更新博客了,在这段时间里发生的事还蛮多的,回想起来才发现时间过得好快啊。至于博客嘛,天天都会关注,一来想看看大家是否写了一些评论,二来想看看大神们(如lufy,浅墨,雾央,Himi等)有没有写新的文章。不过最近没有什么新“发明”,所以就没什么好写的了~Ok,进入正题。


一,说在前面的话

最近有的朋友说他们很喜欢原生的javascript的代码,不喜欢看用引擎封装后的,所以希望我写一些原生html5代码。于是我原本就准备在这篇中讲讲用原生html5写游戏,但是写过来写过去发现还得用到lufylegend中的一些原理。于是就想到可以讲讲lufylegend中的一些原理。毕竟知道原理比只知道用法好得多。首先我们得看看lufylegend引擎的介绍。

【lufy在以前的文章中也提到过lufylegend的原理,但我个人认为介绍地不详细,没有说清楚原理,后来我自己慢慢看,才看懂了一些。所以我希望我能通过文章的形式来谈谈自己对lufylegend的理解,如果文章中有疏漏的地方,欢迎提出~】

二,lufylegend.js引擎介绍

这是官方的介绍:

lufylegend是一个HTML5开源引擎,它实现了利用仿ActionScript3.0的语法进行HTML5的开发, 包含了LSprite,LBitmapData,LBitmap,LLoader,LURLLoader,LTextField,LEvent等多个AS开发人员熟悉的类, 支持Google Chrome,Firefox,Opera,IE9,IOS,Android等多种热门环境。 利用lufylegend可以轻松的使用面向对象编程,并且可以配合Box2dWeb制作物理游戏, 另外它还内置了LTweenLite缓动类等非常实用的功能, 现在开始使用它吧,它可以让你更快的进入HTML5的世界!

从上面的文字可以看出啊,lufy大神的文字功底还是很屌的。其实说白了,lufylegend是一个兼容性极高,功能极多,使用方便的HTML5游戏引擎。

那么,在众多html5游戏引擎中,lufylegend优势何呢?

首先我觉得,最扯的一个优势就是:Lufylegend is made by Chinese。当然Cocos2d-html5也是made by Chinese啊~但是,lufylegend的API文档是中文的,Cocos2d-html5就不一样了。也许你非常懂英文,但是至少而言,你看中文还是比看英文习惯一些,不是吗?

第二个优点就是它效率十分高。在单核CPU上跑,FPS在15-23之间,在手机也就差不多是这个档次了。也许有朋友笑曰:“我X,FPS在15-23之间好意思说出口?”,但其实不然,你要清楚啊,html5和OpenGL等不是一个档次的,OpenGL发展N年了,而html5仍然被认为是一项新技术。以前,我也试着去学习Cocos2d-html5,但说真的,效率感觉比不上lufylegend。

第三个优点就是配置简单。有些引擎,东西一大堆,使用者根本无从下手。而使用lufylegend,只需要把lufylegend-xxx.min.js复制到项目下面即可。

其实说实话,只有你使用它以后,才能真正地感受到带来的方便。

当然,我还是再次把引擎的地址display出来:

官方网站:http://lufylegend.com/lufylegend

API文档:http://lufylegend.com/lufylegend/api

引擎的logo:

[置顶]        HTML5游戏引擎lufylegend深入浅出 - 引擎介绍&amp;原理

三,引擎原理

我们知道,游戏主要由事件和画面组成,在lufylegend的事件中,有鼠标事件(MOUSE_DOWN,MOUSE_UP,MOUSE_MOVE),键盘事件(KEY_DOWN,KEY_UP),时间轴事件(ENTER_FRAME),这些事件中,前两项so easy,好理解,但时间轴事件对于一些刚接触游戏开发的新生而言,有些摸不着头脑。其实时间轴事件相当于一个定时器,这个事件的监听者(listener,也就是事件回调函数)每隔一段时间就会触发一次。但这个东西有什么用呢?我们举个例子吧,假如我们做一个飞机大战的游戏,要让敌机缓缓地移动起来,如果我们直接将它们的x或者y设置为某值,那飞机就会在嗖地一声在瞬间之内移动到那里。这样很不“礼貌”,因为动作太大了~我们要做到缓缓地移动,这时候时间轴事件就该派上用处了,我们可以在监听函数中给飞机的x或者y增加某个值,这样的话,监听函数每被调用一次,就会将飞机移动一下,又因为每隔一段时间监听函数才被调用一次,所以说飞机就能够达到慢慢移动的效果,这样就”礼貌“多了,对吧?当然,要让飞机移动地更帅一点,那就要用到缓动类LTweenLite,什么是缓动类?好吧,我直观地告诉你,就是像jQuery淡入淡出那种逐渐变化从而实现某种效果的一个功能。这里有lufy对缓动类进行巧妙地利用,做出的一个超帅动画:HTML5超帅动画制作-LTweenLite的妙用

说完事件,我们再来说说画面。lufylegend中,但凡是加入到画面上的显示对象都是通过一个setInverval不停绘制的,这样做有什么必要呢?首先,是可以实现层次化,如果我们把所有对象放入一个数组中,通过遍历的方式获取每个对象并调用函数将其显示,那么第一个被加入的对象就会先被画在最下面,其余的依次画上,这样一来就实现了层次化效果。其次,还有个好处就是可以通过直接更改对象的属性从而在下次重绘时表现出来,比如说我们加入一个图片对象(对象名字为img),这时我们要改变它的显示方式为不显示(visible属性改为false),那么直接把img.visible改为false即可,那在下次重绘时就控制它不显示。另外,在上面提到的时间轴事件触发的速度也是由重画速度决定的——每画一次就调用一次。

在lufylegend中,但凡是可显示的对象,大都继承自LDisplayObject。这个类有个ll_show方法,用于在循环渲染时变幻画布,绘制该显示的东西。

四,利用引擎初始化游戏

在引擎中,要初始化游戏需要用到引擎内部的init函数,使用方法举例如下:

init(50,"mylegend",800,480,main)

这个函数的参数是:

init(speed,divid,width,height,completeFunc);

speed:游戏速度设定

divid:传入一个div的id,库件进行初始化的时候,会自动将canvas加入到此div内部

width:游戏界面宽

height:游戏界面高

completeFunc:游戏初始化后,调用此函数

在使用lufylegend时,不用在html文件中写什么<canvas>标签,不过要写一个div,如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
<div id="mylegend">loading……</div>
<script type="text/javascript" src="../lufylegend-x.x.x.min.js"></script> 
<script>
init(50,"mylegend",800,480,main);
function main(){
    alert("感谢您使用lufylegend库件");
}
</script>
</body>
</html>

还是很简单的,不是吗?不过值得一提的是,init的参数speed或许有的朋友不理解什么是游戏速度,其实就是我在原理中介绍到的setInterval的速度,这个速度控制的是重绘速度和时间轴触发速度,如果设得超大,你的画面会很卡,不管你是什么双核CPU还是什么四核CPU。如果设得超小,比如说1-10,那有些性能差的电脑显示起来就会有些差,一卡一卡的~所以我一般设置为30-50,这之间的数应该都挺合适的。

五,引擎基本功能及原理

1,LGlobal(LStage、LSystem)静态类

这个类掌管游戏中许多全局的设置,比如说更改游戏速度,获取游戏界面高度、宽度,canvas标签,canvas标签的getContext("2d"),和当前操作系统等,可以到API文档中看个究竟。

在LGlobal中,有个childList属性,这个属性在最新的几个版本里面主要存放LGlobal.stage(最底层,一个LSprite对象。LSprite是什么?下文会慢慢介绍的),LTweenLite(缓动类),LSound.Container(音频容器,貌似里面有用于判断音频是否结束的函数)。引擎循环渲染便是从这个地方开始的。引擎中通过遍历LGlobal.childList获取这几个对象,然后调用每个对象的ll_show方法来实现循环执行每个对象应该执行的命令,例如LGlobal.stage就应该执行LSprite该做的向下循环子对象,LTweenLite就应该调用缓动函数来改变缓动数据等。

2,全屏设置

html5最大的优势就是跨平台,所以游戏要能在手机上运行最好,但是能运行不能就完了,还要能达到全屏。以前lufylegend的全屏有点问题,不过最近改好了,不仅能手机全屏,PC机也OK,还提供了三种全屏模式:

LStageScaleMode.EXACT_FIT:[静态] 指定整个应用程序在指定区域中可见,但不尝试保持原始高宽比。

LStageScaleMode.SHOW_ALL:[静态] 指定整个应用程序在指定区域中可见,且不会发生扭曲,同时保持应用程序的原始高宽比。

LStageScaleMode.NO_SCALE:[静态] 指定应用程序的大小是固定的,因此,即使在更改播放器窗口大小时,它仍然保持不变。

※设置完舞台的缩放模式之后,调用LSystem.screen(LStage.FULL_SCREEN);就可以实现全屏。

原理很简单,就是设置canvas标签的style.width和style.height来实现GPU缩放。

3,加入对象/移除对象:addChild,removeChild

在lufylegend中要加入显示对象到银幕上,需要使用addChild,如果直接调用这个函数,就是把对象加入最底层,当然LSprite也有addChild,是把对象添加到LSprite上,从而实现层次化效果。

removeChild是一样的,只不过是把对象删除而已~

addChild和removeChild等同于LGlobal.stage.addChild和LGlobal.stage.removeChild。原理为把参数所指定的对象从LSprite的childList里面删除掉。

4,LLoadManage加载文件

主要用于加载游戏中的文件,如图片,音频,js文件,其他文件(需要用到服务器),用法可查API文档。

原理:通过判断文件不同的类型从而分别调用LLoader(读取图片),LURLLoader(读取文本文件,包括js文件),LSound(读取音频)。当然你可以直接用上述的几个类读取数据,但是咱们的LLoadManage胃口更大,可以批量读取不同类型的文件,使用起来还会更方便,因为其中还提供了进度条的显示。

5,图片类LBitmap,LBitmapData

这个两个类主要配合显示图片,不过显示之前需要加载图片,方法如下:

var loadData = [
{name:"yorhom",path:"./images/yorhom.png"}
];
var datalist=[]; 
function main(){ 
    loadingLayer = new LoadingSample1(); 
    addChild(loadingLayer); 
    LLoadManage.load( 
        imgData, 
        function(progress){ 
            loadingLayer.setProgress(progress); 
        }, 
        gameInit 
    ); 
} 
function gameInit(result){ 
    datalist = result; 
    removeChild(loadingLayer); 
    loadingLayer = null; 
    
	var bitmapData = new LBitmapData(datalist["yorhom"]);
	var bitmap = new LBitmap(bitmapData);
	addChild(bitmap);
}

通过这俩的名字可以看出来,一个是负责提供数据,一个负责按数据要求显示,类似于教研员和考生。LBitmap是LDisplayObject的子类,在ll_show中会调用到绘制图像的函数。同时也会进行相应的画布变幻。

6,LSprite层次化效果

LSprite的定义是:LSprite 类是基本显示列表构造块,一个可显示图形并且也可包含子项的显示列表节点。这个有点复杂,但其实说白了就是一个Layer层。我们在前面说到过,加入越后的对象会显示在最顶部,但是我要让后加的对象显示在先加的对象下方,就要用到LSprite分层显示了。LSprite也有addChild和removeChild方法,用于添加对象。此外,LSprite还有addEventListener函数,用于给对象加入事件。具体的一些功能可以到官方API去看看。

LSprite也有个childList属性,装有其子成员,在ll_show中,LSprite执行画布变幻和循环自己的childList,然后调用循环到的子对象的ll_show方法。

7,LTextField文字显示

这个类如LBitmap一般,需要用addChild加入到界面中。这个类的属性很多,不过用起来挺不错的,大家可以到API中去看一看,使用举例如下:

init(20,"mylegend",500,400,main); 
var backLayer,title; 
function main(){ 
    backLayer = new LSprite(); 
    addChild(backLayer); 
    title = new LTextField(); 
    title.size = 30; 
    title.color = "#ff0000"; 
    title.text = "文字显示测试"; 
    backLayer.addChild(title); 
}

这个类嘛和LBitmap执行的命令差不多,画图的命令改为画文字的命令即可。

六,CSDN博客之星评选活动【已过时,大家忽略掉吧】

我原本想特此写一篇文章拉票的,但是我觉得这样不妥,因为我很久没写文章了,结果好不容易发一个却是一个广告,这样没意义,对吧~所以呢我还是坚持分享一下自己的收获,于是写下了这篇文章。当然,如果您认为我做出的努力对你有帮助,不妨投我一票吧:http://vote.blog.csdn.net/blogstaritem/blogstar2013/yorhomwang 最后的结果对我而言并不重要,但是大家的支持是我继续更新博客的动力~

Ok,就说到这里,搞定,收工~

本章就到此为止,以上就是本篇所有内容,欢迎大家交流。

—————————————————————-

欢迎大家转载我的文章。

转载请注明:转自Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

分类: html5 标签:

『HTML5梦幻之旅』-滚动播放的幻灯片效果

2013年10月20日 没有评论

很久又没写博客了,一者是因为要上课,平时没有什么零碎的时间研究新东西;二者最近在开发一款塔防游戏,有整块的时间都拿去开发这个项目了。因此最近没有什么好的东西和大家分享。这周末看在游戏快完工的份上,抽出了半天的时间研究了一下html5幻灯片制作,效果还不错,展示如下:

首先是一张《真三国无双7》关云长的头像

『HTML5梦幻之旅』-滚动播放的幻灯片效果

其次是《真三国无双7》貂蝉的头像

『HTML5梦幻之旅』-滚动播放的幻灯片效果

切换时的效果

『HTML5梦幻之旅』-滚动播放的幻灯片效果

怎么样?效果还是很不错,对吧~

测试链接:http://www.cnblogs.com/yorhom/articles/html5_dream_trip_slideshowsample1.html

接下来就来讲一讲制作过程。

一,准备工作

首先,需要下载html5开源引擎lufylegend.js,它是完全免费+开源的,可以下载下来学习一下。

下载地址:http://lufylegend.com/lufylegend

API文档:http://lufylegend.com/lufylegend/api

二,设计原理

要搞清楚本次开发的原理,我们不妨先画一张示意图:

『HTML5梦幻之旅』-滚动播放的幻灯片效果

假设绿色方框内的区域为显示区域,那么,我们可以用到lufylegend中的mask的方法来将整个红色方框裁减为绿色方框的大小,这时,我们只能看到A区域,而B,C被遮盖住了。然后如果我们要显示B区域,我们可以直接将红框区域往左移动一格的距离,绿色区域不动,就能将B显示出来了,就像放映机一样。

但是如果一直播放下去,那么我们播放到C区域时,红色区域再往左移动,那就会显示为空白。解决措施是又移回A区域,但是出现的问题又在于红色区域如果要移到A处,那应该右移动才能到达,但是我们要的是红色区域往左移动,因此就不能直接移回。那该怎么办呢?我想的解决措施就是把最前面的那个区域移动到最后的那个区域的右边。如果是刚才那种情况,那就让A接到C后方,这时候,A变成了最后一个。这时红色区域又往左移动时,取到的就是B,然后将B移动到A后面。

当然,上面我们只提到了向左移动时的处理方式,其实向右也是一样的,只是取的是最后一个区域,然后把这个区域移动到第一个区域的前面罢了。

光说原理只能算是纸上谈兵,接下来就来看看代码。

三,代码讲解

首先我们为了方便起见,建立一个LSlideshowSample1类,因为以后万一要拓展别的幻灯片效果,所以就在这个类的名字后面写上了"Sample1”,名字只是一种代号,我们主要看代码,构造器如下:

function LSlideshowSample1(width,height,isLoop,loopData){
	var self = this;
	base(self,LSprite,[]);
	
	self._slideshowList = new Array();
	self._slideshowIndex = 0;
	self._currentIndex = self._slideshowIndex;
	
	self.rangeWidth = width;
	self.rangeHeight = height;
	self.isLoop = isLoop;
	
	self._toX = 0;
	
	self.nextChildList = new Array();
	self.previousChildList = new Array();
	
	self.borderLayer = new LSprite();
	self.borderLayer.graphics.drawRect(0,"",[0,0,width,height],true,"transparent");
	
	self.contentLayer = new LSprite();
	self.contentLayer.mask = self.borderLayer;
	self.addChild(self.contentLayer);
	
	if(self.isLoop == true){
		self.loopData = loopData;
		self.frameIndex = 0;
		self.maxFrame = Math.floor(self.loopData.delay*1000/LGlobal.speed);
		self.addEventListener(LEvent.ENTER_FRAME,self.loopPlay);
	}
}

这个类有4个参数,意思分别是:[显示宽度,显示高度,是否自动播放,自动播放方式],如果第三个参数填false,就可以不用添第4个参数。

接下来解释一下构造器中的代码:
self._slideshowList原先是用来装每一帧的数据的,后来直接在LSprite的childList中取了,所以它没有什么用,直接忽略掉。

接下来看_slideshowIndex属性,这个属性很怪异,主要用于取出要显示的帧,以后慢慢解释。_currentIndex是用来表示显示位置的,如果往左移动,表示显示位置增加1格,往右移动,表示显示位置减少1格。

self.rangeWidth,self.rangeHeight,self.isLoop是将参数存放起来的属性,后面会用到的。

然后是self._toX,这个表示要移动到的位置,和_currentIndex联用,以后也会提到,到时候慢慢讲。接下来是self.nextChildList和self.previousChildList属性,这两个属性和_slideshowIndex联用,_slideshowIndex主要负责作为这两个数组的取值下标。接下来,我们初始化显示层:

self.borderLayer = new LSprite();
self.borderLayer.graphics.drawRect(0,"",[0,0,width,height],true,"transparent");
	
self.contentLayer = new LSprite();
self.contentLayer.mask = self.borderLayer;
self.addChild(self.contentLayer);

borderLayer等于上面我们讲解原理时的绿色区域,contentLayer代表红色区域。borderLayer要作为contentLayer的遮罩,因此在contentLayer中写道:

self.contentLayer.mask = self.borderLayer;

然后进入判断是否自动播放:

if(self.isLoop == true){
	self.loopData = loopData;
	self.frameIndex = 0;
	self.maxFrame = Math.floor(self.loopData.delay*1000/LGlobal.speed);
	self.addEventListener(LEvent.ENTER_FRAME,self.loopPlay);
}

代码很简单,值得注意的是,我们实例化构造器时,第4个参数是一个json对象,如下格式:

{delay:每帧停留时间(单位s),order:出现时滚动的样式(向右或者向左)}

ok,构造器就讲完了,接下来看看设置帧的setFrame:

LSlideshowSample1.prototype.setFrame = function(o){
	var self = this;
	var cl = self.contentLayer.childList;
	o.x = self.contentLayer.childList.length * self.rangeWidth;
	o.y = 0;
	self.contentLayer.addChild(o);
	
	self._sortChild();
};

这个函数也有一个参数,是一个LDisplayObject对象。其中代码很简单,就是将加入的这一帧放到最后面。其中调用到一个_sortChild函数,这个函数十分重要,如下:

LSlideshowSample1.prototype._sortChild = function(){
	var self = this;
	
	self.nextChildList = new Array();
	self.previousChildList = new Array();
	var duplicate = new Array();
	for(var i=0; i<self.contentLayer.childList.length; i++){
		self.nextChildList.push(i);
		duplicate.push(i);
	}
	self.nextChildList = self.nextChildList.sort();
	duplicate.splice(0,1);
	var sortedList = duplicate.sort(function(a,b){
		return b - a;
	});
	self.previousChildList.push(0);
	for(var key in sortedList){
		self.previousChildList.push(sortedList[key]);
	}
};

我们能看到,主要是对self.nextChildList和self.previousChildList两个属性进行操作。也许有朋友不理解为什么要将他们排续,这个就设计到显示时的原理,还是用刚才那张图来讲解:
『HTML5梦幻之旅』-滚动播放的幻灯片效果

我们加入ABC后,A在contentLayer中的成员序号是0,B是1,C是2。在self.nextChildList我们将其加入先后顺序排列为[0,1,2],如果我们要显示B区域,那我们就要在contentLayer中取B对象,B对象的序号是1,因此我们要将显示位置往后调一格,于是self._slideshowIndex+=1; self._currentIndex += 1; 然后通过self._slideshowIndex的值来取出contentLayer的成员列表中对应的对象,正好self._slideshowIndex初始值是0,+1后变为1,取出的刚好是B对象。

但是,我们的红色区域如果往右移动时,按理说要显示最后的那一个对象C,C的序号是2,但是self._slideshowIndex – 1 != 2,因此我们要在新建self.previousChildList这个属性来确保往右移动时,能正常地取出对象的序号,因此我们对self.previousChildList里的成员排列做了特殊处理,将其变成了[0,2,1]。但是还是不对啊,self._slideshowIndex – 1 = -1时,应该显示C对象,但是C对象序号在self.previousChildList中对应的下标是1不是-1,怎么办呢?其实很简单,取的时候用self._slideshowIndex的绝对值就ok啦。

比如说我们F5一下界面,回到第一格的位置,然后,我们将红色部分向有移动一格,self._slideshowIndex -= 1; self._currentIndex -= 1; 现在按理说要显示C,这时我们在self.previousChildList中用self._slideshowIndex的绝对值取出要找的对象,self._slideshowIndex这时的值是-1,绝对值就是1,在[0,2,1]中对应的值正好是2,也就是C在contentLayer的成员列表中对应的序号,然后用这个序号取出contentLayer的成员列表中对应的成员,位置移动到响应的地方,然后将其通过LTweenLite缓动类缓缓地显示出来,达到想要的效果。

有了上面的介绍,我们就来看看显示部分的代码next()和previous():

LSlideshowSample1.prototype.next = function(){
	var self = this;
	
	self._currentIndex += 1;
	self._slideshowIndex += 1;
	
	if(self._slideshowIndex >= self.contentLayer.childList.length){
		self._slideshowIndex = 0;
	}
	
	if(self._slideshowIndex < 0){
		var obj = self.contentLayer.getChildAt(self.previousChildList[Math.abs(self._slideshowIndex)]);
	}else{
		var obj = self.contentLayer.getChildAt(self.nextChildList[Math.abs(self._slideshowIndex)]);
	}
	obj.x = self.rangeWidth*self._currentIndex;
			
	self._toX = -(self._currentIndex*self.rangeWidth);
	
	var tweenObj = LTweenLite.to(self.contentLayer,1,{
		x:self._toX
	});
};
LSlideshowSample1.prototype.previous = function(){
	var self = this;
	
	self._currentIndex -= 1;
	self._slideshowIndex -= 1;
	
	if(self._slideshowIndex < -(self.contentLayer.childList.length-1)){
		self._slideshowIndex = 0;
	}
	
	if(self._slideshowIndex < 0){
		var obj = self.contentLayer.getChildAt(self.previousChildList[Math.abs(self._slideshowIndex)]);
	}else{
		var obj = self.contentLayer.getChildAt(self.nextChildList[Math.abs(self._slideshowIndex)]);
	}
	obj.x = self.rangeWidth*self._currentIndex;
		
	self._toX = -(self._currentIndex*self.rangeWidth);
	
	var tweenObj = LTweenLite.to(self.contentLayer,1,{
		x:self._toX
	});
};

其中用到了LTweenLite,这个是Lufylegend中给的一个用于实现缓动的类,具体方法请移步API文档。

还有一个getChildAt,这是lufylegend中LSprite类的一个成员函数,用于取出参数值在LSprite成员列表中相应的对象。

这个类在设计时为了方便大家使用,还提供了showFrameAt函数,可以直接跳到播放某一帧:

LSlideshowSample1.prototype.showFrameAt = function(index,order){
	var self = this;
	if(self._slideshowIndex < 0){
		if(self.previousChildList[Math.abs(self._slideshowIndex)] == index)return;
	}else{
		if(self.nextChildList[Math.abs(self._slideshowIndex)] == index)return;
	}
	if(order == LSlideshow.LEFTWARD){
		self._currentIndex -= 1;
	}else if(order == LSlideshow.RIGHTWARD){
		self._currentIndex += 1;
	}else{
		self._currentIndex += 1;
	}
	self._slideshowIndex = index;
	
	var obj = self.contentLayer.getChildAt(index);
	obj.x = self.rangeWidth*self._currentIndex;
	
	self._toX = -(self._currentIndex*self.rangeWidth);
	
	var tweenObj = LTweenLite.to(self.contentLayer,1,{
		x:self._toX
	});
};

在上面也提到过自动播放这个功能,我们不妨温习一下调用自动播放的地方:

if(self.isLoop == true){
	self.loopData = loopData;
	self.frameIndex = 0;
	self.maxFrame = Math.floor(self.loopData.delay*1000/LGlobal.speed);
	self.addEventListener(LEvent.ENTER_FRAME,self.loopPlay);
}

在时间轴事件ENTER_FRAME中,我们调用了loopPlay函数,这个函数的代码如下:

LSlideshowSample1.prototype.loopPlay = function(self){
	if(self.contentLayer.childList.length == 0)return;
	if(self.frameIndex++ < self.maxFrame)return;
	self.frameIndex = 0;
	if(self.loopData.order == LSlideshow.RIGHTWARD){
		self.next();
	}else if(self.loopData.order == LSlideshow.LEFTWARD){
		self.previous();
	}else if(self.loopData.order == LSlideshow.RANDOM){
		var index = Math.floor(Math.random()*(self.contentLayer.childList.length-1));
		var order = Math.random() > 0.5 ? LSlideshow.LEFTWARD : LSlideshow.RIGHTWARD;
		self.showFrameAt(index,order);
	}
};

其中我们能看到了LSlideshow.RIGHTWAR,LSlideshow.LEFTWARD,LSlideshow.RANDOM这几种播放方式,它们分别是在LSlideshow静态类中得到定义的。如下:

var LSlideshow = function(){throw "LSlideshow cannot be instantiated";};
LSlideshow.type = "LSlideshow";
LSlideshow.RIGHTWARD = "rightward";
LSlideshow.LEFTWARD = "leftward";
LSlideshow.RANDOM = "random";

LSlideshow.RIGHTWAR代表向右滚动,LSlideshow.LEFTWARD代表向左滚动,LSlideshow.RANDOM代表随机滚动。

最后还加了一个getFrameIndex的函数,是用于取当前帧的序号的:

LSlideshowSample1.prototype.getFrameIndex = function(){
	var self = this;
	if(self._slideshowIndex < 0){
		var v = self.previousChildList[Math.abs(self._slideshowIndex)];
	}else{
		var v = self.nextChildList[Math.abs(self._slideshowIndex)];
	}
	return v;
};

ok,幻灯片类就搞定了~只要搞清楚原理,其实还是挺简单的,不是吗?接下来是使用举例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>幻灯片效果</title>
</head>
<body>
<div id="mylegend">loading......</div>
<script type="text/javascript" src="./lufylegend-1.8.0.min.js"></script>
<script type="text/javascript" src="./lufylegend.ui-0.2.0.min.js"></script>
<script type="text/javascript" src="./LSlideshow.js"></script>
<script type="text/javascript" src="./LSlideshowSample1.js"></script>
<script>
init(50,"mylegend",980,609,main);
var backLayer,slideshowObj;
var loadData = [
	{name:"caocao",path:"./images/cao_cao.jpg"},
	{name:"diaochan",path:"./images/diao_chan.jpg"},
	{name:"guanyu",path:"./images/guan_yu.jpg"},
	{name:"zhaoyun",path:"./images/zhao_yun.jpg"},
	{name:"sunshangxiang",path:"./images/sun_shang_xiang.jpg"}
];
var datalist = {};
function main(){
	LStage.setDebug(true);
	if(LGlobal.canTouch){
		LGlobal.stageScale = LStageScaleMode.EXACT_FIT;
		LSystem.screen(LStage.FULL_SCREEN);
	}
	
	var loadingLayer = new LoadingSample5();
	addChild(loadingLayer);
	
	LLoadManage.load(
		loadData,
		function(p){
			loadingLayer.setProgress(p);
		},
		function(r){
			datalist = r;
			removeChild(loadingLayer);
			initPage();
		}
	);
}	
function initPage(){
	backLayer = new LSprite();
	addChild(backLayer);
	
	slideshowObj = new LSlideshowSample1(971,609,true,{delay:3,order:LSlideshow.RANDOM});
	backLayer.addChild(slideshowObj);
	
	slideshowObj.setFrame(new LBitmap(new LBitmapData(datalist["caocao"])));
	slideshowObj.setFrame(new LBitmap(new LBitmapData(datalist["diaochan"])));
	slideshowObj.setFrame(new LBitmap(new LBitmapData(datalist["guanyu"])));
	slideshowObj.setFrame(new LBitmap(new LBitmapData(datalist["zhaoyun"])));
	slideshowObj.setFrame(new LBitmap(new LBitmapData(datalist["sunshangxiang"])));
	
	addButton();
}
function addButton(){
	var next_btn = new LButtonSample2(">",20,"黑体","white");
	next_btn.backgroundCorl = "#008800";
	next_btn.x = LStage.width-next_btn.getWidth()-40;
	next_btn.y = (LStage.height-next_btn.getHeight())*0.5;
	backLayer.addChild(next_btn);
	next_btn.addEventListener(LMouseEvent.MOUSE_DOWN,function(){
		slideshowObj.next();
	});
	
	var last_btn = new LButtonSample2("<",20,"黑体","white");
	last_btn.backgroundCorl = "#008800";
	last_btn.x = 20;
	last_btn.y = (LStage.height-last_btn.getHeight())*0.5;
	backLayer.addChild(last_btn);
	last_btn.addEventListener(LMouseEvent.MOUSE_DOWN,function(){
		slideshowObj.previous();
	});
	
	for(var i=0; i<5; i++){
		var page_btn = new LButtonSample2(i+1,20,"黑体","white");
		page_btn.backgroundCorl = "#008800";
		page_btn.x = 50*i+600;
		page_btn.y = LStage.height-page_btn.getHeight()-40;
		backLayer.addChild(page_btn);
		page_btn.addEventListener(LMouseEvent.MOUSE_DOWN,function(event,o){
			var textObj = o.getChildAt(1).getChildAt(0);
			var toIndex = parseInt(textObj.text)-1;
			slideshowObj.showFrameAt(toIndex,LSlideshow.LEFTWARD);
		});
	}
}
function ondown(event){
	if(event.offsetX <= 100){
		slideshowObj.previous();
	}else if(event.offsetX >= LStage.width-100){
		slideshowObj.next();
	}
}
</script> 
</body>
</html>

运行代码后,就得到了本文最上面展示的效果。

四,源代码

上面讲解得有些乱,大家可以把源代码下载下来看看。

最后,奉上源代码:http://files.cnblogs.com/yorhom/Slideshow_source.zip

本章就到此为止,以上就是本篇所有内容,欢迎大家交流。

—————————————————————-

欢迎大家转载我的文章。

转载请注明:转自Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

分类: html5 标签:

《HTML5游戏框架之CraftyJS》一个简单乒乓球游戏的例子

2013年3月1日 没有评论

CraftyJS是一个GPL和MIT双授权的基于Javascript 的开源HTML5框架,其最大的特点是没有使用典型的类和继承体系,而是为开发者提供了基于实体(entity)和组件(component)的结构来组织代码。这种组织结构能给你的代码带来很大的灵活性。

官网地址:http://craftyjs.com/

首先说说CraftyJS编程的基本结构:

实体

游戏中所有的对象都被看作实体,比如文本、玩家等等。我们使用Crafty.e()来创建一个实体,比如

var myEntity = Crafty.e(“2D”);

上面的代码就创建了一个实体,这个实体只拥有一个组件“2D”。那么组件是什么?

 

组件

组件用于为实体提供功能,所谓提供功能其实就是为实体提供了属性和方法,在上面的例子中”2D”就是一个组件,且是Crafty提供的一个预定义的组件,“2D”组件能够使你的实体拥有坐标,高、宽等等基本属性,我们可以设置这些属性来设置实体的位置,比如:

myEntity.attr(x:200,y:200,w:10,h:100);

上面的代码中.attr()是一个函数用来设置实体的各个属性,这个函数由Crafty的核心对象提供(以后再详细说),这里我们将实体的位置设置为(200,200)的位置上,w代表实体的宽,h代表实体的高。

 

现在我们有了一个实体,但实体听不会绘制在canvas上,我们还需要其他的组件来完成这个任务。

 

Canvas和DOM

这两个预定义的组件分别提供了两种绘制实体的方法,看名字就知道,一种是用canvas来绘制实体,一种是用DOM来绘制实体。下面使用Canvas来重新定义我们的实体:

var myEntity = Crafty.e(“2D,Canvas”);

myEntity.attr(x:200,y:200,w:10,h:100);

这样能将我们的实体以canvas 的方式绘制在屏幕,如果像使用DOM来绘制实体只要更换组件即可。

var myEntity = Crafty.e(“2D,DOM”);

myEntity.attr(x:200,y:200,w:10,h:100);

注意:这里还有个颜色问题,上面的代码我没有试验,但以我之前的经验,用canvas绘制的实体默认颜色是黑的,用DOM时默认是白的,如果想设置颜色需要使用另一个名叫Color的组件,这个组件提供了.color()函数用来设置实体属性。

 

看到这里就可以明白为什么说Crafty非常灵活,如果以后想支持SVG绘图,那只要再定义一个SVG组件就行了。

这里说的都是使用组件,组件也是可以自定义的,Crafty.c()函数定义,因为例子里用不到所以先不说,

 

下面是乒乓球游戏的代码(为了更简单把官网上的例子简化了一下,没有使用记分牌)

 

Crafty.init(600, 300);
Crafty.background('rgb(127,127,127)');

//球拍(其实就是两个矩形。。)
Crafty.e("Paddle, 2D, DOM, Color, Multiway")
	.color('rgb(255,0,0)')
	.attr({ x: 20, y: 100, w: 10, h: 100 })
	.multiway(4, { W: -90, S: 90 });
Crafty.e("Paddle, 2D, DOM, Color, Multiway")
	.color('rgb(0,255,0)')
	.attr({ x: 580, y: 100, w: 10, h: 100 })
	.multiway(4, { UP_ARROW: -90, DOWN_ARROW: 90 });

//球
Crafty.e("2D, DOM, Color, Collision")
	.color('rgb(0,0,255)')
	.attr({ x: 300, y: 150, w: 10, h: 10, 
			dX: Crafty.math.randomInt(2, 5), 
			dY: Crafty.math.randomInt(2, 5) })
	.bind('EnterFrame', function () {
		//hit floor or roof
		if (this.y <= 0 || this.y >= 290)
			this.dY *= -1;

		if (this.x > 600) {
			this.x = 300;
		}
		if (this.x < 10) {
			this.x = 300;
		}

		this.x += this.dX;
		this.y += this.dY;
	})
	.onHit('Paddle', function () {
	this.dX *= -1;
})

 

首先说说代码中有但我还没有说的东西,首先是

Crafty.init(600,300);

初始化代码,必须有,用来创建游戏舞台,600表示舞台的宽,300表示舞台的高。

Crafy.background(“rgb(127,127,127)”));

这段代码用来设置背景色,背景色被设置为灰色。

然后是组件Paddle和Collision

组件Paddle是一个未定义的组件,不提供任何功能,仅仅其标识作用,这里我们知道可以为实体添加一个未定义的组件。这个组件仅仅在系统提供的预定义组件Collision的onHit()函数中用到,Collision组件用于碰撞检测,提供的onHit()函数在产生碰撞后将调用其回调函数。

 

还有一个MultiWay组件,是用来设置键盘响应的,就是用键盘来控制两个球拍,这个组件提供了multiway()函数。

 

关于游戏逻辑

玩一下游戏就知道了= =,w和s键控制左球拍,右边方向键控制右球拍。

分类: html5 标签: