02

2011年4期《程序员》配套源码及相关链接

作者: baiyuzhong 分类:架构实践   阅读:24,877 次 添加评论

为了方便大家查阅,现把2011年4期《程序员》杂志中相关链接及代码发布在此


P91

RCFile—用于Facebook数据仓库的高效存储结构

注:

【1】 hello-world程序的唯一作用就是显示出“Hello, world!”这句话。使用Java语言,你需要这样写:

public class Hello {

public static void main(String[] args) {

System.out.println(“Hello, world!”);

}

}

P101

调整Netfilter内核模块以限制P2P连接

代码1:

注册一个钩子函数是一个围绕nf_ hook_ ops结构体的很简单的过程,在linux/netfiIter.h中有这个结构体的定义:

struct nf_hook_ops

{

struct list_head list;

nf_hookfn *hook;

int pf;

int hooknum;

int priority;

};

P102-103

代码2:

#iptables -t nat -a POSTROUTING -o eth0 -s 192.168.0.0/16 -j MASQUERADE

#iptables -a FORWARD -i eth0 -m state –state ESTABLISHED RELATED -j ACCEPT

#iptables -a FORWARD -s 192.168.0.0/16 -j ACCEPT

#iptables -a FORWARD -j ACCEPT

该Linux服务器已通过iptables工具配置好了NAT以及一些访问策略等,这里说明一下FORWARD链的配置,该链至少要有如下配置:

代码3:

模块流程图4如图四所示:

主要代码如下:

static struct nf_hook_ops nfho;

/*实施连接限制的IP地址段,以网络字节顺序表示*/

static unsigned long lim_min_ip=0x0201A8C0;//”192.168.1.2″

static unsigned long lim_max_ip=0xFEFFA8C0;//”192.168.255.254″

static int lim_conn=20;

/*每IP限制的连接数为20*/

static int count_conntrack_iterate(const struct ip_conntrack_tuple_hash * hash,const unsigned long our_ip,int * pcount)

{

struct ip_conntrack * ct=hash->ctrack;

int protonum;

IP_NF_ASSER(hash->ctrack);

MUST_BE_READ_LOCKED (&ip_conntrack_lock);

/*只考虑原方向上的连线跟踪*/

if(DIRECTION(hash))

return 0;

protonum=ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.dst.protonum;

if(protonum I=17 && protonum I=6)

/*如果不是TCP和UDP连接*/

return 0;

/*若连线跟踪元组里面的源IP地址为当前数据包的IP地址,则计数加1*/

if(cur_ip == ct->tuplehash [IP_CT_DIR_ORIGINAL].tuple.src.ip)

(*pcount)++;

return 0;

}

static void count_conntrack

(const unsigned long ip,int* pcount)

{

unsigned int i;

READ_LOCK(&ip_conntrack_lock);

/*遍历整个连线跟踪元组哈希表*/

for (i=0;i<ip_conntrack_htable_size;i++)

{

LIST_FIND(&ip_conntrack_hash[i],

count_conntrack_iterrate,struct ip_conntrack_tuple_hash *,ip,pcount);

}

READ_UNLOCK

(&ip_conntrack_lock);

}

/*钩子函数定义*/

unsigned int limitp2p_hook_func(unsigned int hooknum,struct sk_buff)**skb,const struct net_device *in,const struct net_device *out,int(*okfn)(struct sk_buff *))

{

struct sk_buff *sb+*skb;

/*若不在实施连接限制的IP地址段,交给下一层处理*/

if(sb->nh.iph->saddr<lim_min_ip||sb->nh.iph->saddr>lim_max_ip)

return NF_ACCEPT;

struct ip_conntrack *ct;

enum ip_conntrack_info ctinfo;

/*若有连线跟踪说明该连接已建立或者未建立正等待对方回应*/

if(ct)

return NF_ACCEPT;

int cur_conn=0;

/*根据数据包中源IP地址遍历连线跟踪哈希表获得已建立连线数*/

count_conntrack(sb->nh.iph->saddr.&cur_conn);

if(cur_conn>lim_conn)

return NF_DROP;

else

return NF_ACCEPT;

}

/*模块初始化函数*/

int limitp2p_int_module()

{

nfho.hook=limitp2p_hook_func;

/*钩子函数名*/

nfho.hooknum=NF_IP_PRE_ROUTING;

/*Netfilter框架在IPv4的检查点*/

nfho.pf=PF_INET;

/*采用的协议簇,这里采用IPv4*/

nfho.priority=NF_IP_PRI_FIRST;

/*使编写的函数具有优先调用*/

/*向Netfilter框架登记钩子函数*/

nf_register_hook(&nfho);

return 0;

}

/*模块卸载函数*/

void limitp2p_cleanup_module()

{

/*向Netfilter框架取消钩子函数登记*/

nf_unregister_hook(&nfho);

}

module_init(limitp2p_init_module);

module_exit(limitp2p_cleanup_module);

P107

请参考我们的博客系列文章介绍该项目的实现细节:http://blogs.msdn.com/b/windows-azure-support/archive/2010/08/11/bring-the-clouds-together-azure-bing-maps.aspx,以及自All-In-One Code Framework下载示例源代码:http://1code.codeplex.com/releases/view/59639#DownloadId=201758。

P112

Android自动化测试之道

代码1:

要使用CTS进行兼容性测试,就必须先下载和编译CTS,下载方式可以选择下载完整的Android源码(位于$ANDROID/cts目录中)和只下载cts源码(注:cts源码下载地址为git://android.git.kernel.org/platform/cts.git);然后可以使用如下命令来编译CTS:

//配置选项

$ build/envsetup.sh

//编译cts

$ make cts

代码2:

如果输入“ls  –plan”命令可以查看所有的plan(注:plan文件夹为cts/android-cts/repository/plans),在其中可以看到8个测试基类,比如Android.xml、AppSecurity.xml、CTS.xml等。其中所有基类里包含的package都给出了相应的uri,cts将根据这些uri去测试每个基类里的package,可以输入如下一些命令来测试某些package:

//测试所有cts测试用例

cts_host > start  –plan  CTS

//测试Android API

cts_host > start  –plan  Android

//测试Java核心库

cts_host > start  –plan  Java

代码3:

通常在测试过程中也很可能会出现错误,这时如果需要知道具体是在哪一个plan处出错以及出错的原因,就可以使用“-plan”、“-p”、“-t”、“-e”、“-w”等参数来指定测试某个用例,这样可以节约调试的时间,如下命令可以指定测试AlertDialog:

cts_host > start –plan Android -p android.app -t

android.app.cts.AlertDialogTest#testAlertDialog

P113

代码1:

编写完测试用例还需要修改AndroidManifest.xml文件,首先需要引入测试库,并指明需要被测试的目标package,代码如下:

<uses-library android:name=”android.test.runner” />

<instrumentation android:targetPackage=”com.yarin.HelloWorld”

android:name=”android.test.InstrumentationTestRunner” />

代码2:

因此我们可以通过“adb shell input/sendevent”命令来向设备发送键盘和触摸操作事件,例如,如下命令所发生的事件为:在坐标(00000045,00000032)处点击屏幕,并且保持按下状态:

adb shell sendevent /dev/input/event0: 0003 0000 00000045

adb shell sendevent /dev/input/event0: 0003 0001 00000032

adb shell sendevent /dev/input/event0: 0001 014a 00000001

代码3:

现在要实现自动化测试,就需要把上述这些操作都通过程序来自动完成,这时可以使用Android工具包中的hierarchyviewer工具(hierarchyviewer.jar)或者其他脚本程序。同时Instrumentation中也提供了以send开头的函数接口来实现模拟键盘鼠标,如下代码所示:

//发送指定KeyCode的按键

sendCharacterSync(int keyCode)

//发送指定KeyCode的按键

sendKeyDownUpSync(int key)

//模拟Touch事件

sendPointerSync(MotionEvent event)

//发送字符串

sendStringSync(String text)

P115

volatile与多线程的那些事儿

代码1:

在这种情况下,C/C++中的volatile关键字其实无法对该操作的原子性提供任何保障。

volatile int i = 0;

thread 1

i++;

thread 2

i++;

P116

代码2:

dekker算法如下所示。

volatile int flag1 = 0;

volatile int flag2 = 0;

volatile int turn  = 1;

volatile int gCounter = 0;

void dekker1() {

flag1 = 1;

turn = 2;

while((flag2 == 1) && (turn == 2)) {}

// 进入临界区

gCounter++;

flag1 = 0; // 离开临界区

}

void dekker2() {

flag2 = 1;

turn = 1;

while((flag1 == 1) && (turn == 1)) {}

// 进入临界区

gCounter++;

flag2 = 0; // 离开临界区

}

代码3:

例如在下面这个程序中,两个线程分别执行thread1和thread2,它们通过sleep这个volatile变量来进行同步。

volatile int sleep = 1;

int gData = 0;

void thread1() {

while (sleep) {}

// gData load guaranteed after every load of sleep

printf(“gData = %d\n”, gData);

}

void thread2() {

gData++;   // guaranteed to occur before write to sleep

sleep = 0;

P121

Maven十年,来认识下

这么做是有理由的,较之于从网站找下载链接,Maven用户现在只需要配置pom.xml就可以了:

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>4.7</version>

<scope>test</scope>

</dependency>

P124

非关系型数据库memlink浅析

数据模型如下:

其中,MySQL的数据表为:

CREATE TABLE TestList (

listkey` char(5) not null,

`listvalue` char(10) not null,

`change_time` int unsigned not null,

key `testlist` (`listkey`, ‘change_time`)

)ENGINE=InnoDB DEFAULT CHARSET=utf8;

MySQL查询语句为select listvalue from

TestList where listkey=’$key’ order by

change_time limit $start,$len

Redis命令为LRANGE $key $start $end

memlink命令为RANGE $key $start $len


特别策划部分:

P57 HTML5 2D游戏开发入门——结合实例,一步一步开发2D小游戏

第一节 静态图片的绘制

在开始开发游戏之前, 首先要了解一下如何利用HTML5来绘制静态的图片, 这是开发2D游戏的基础。在HTML5 Canvas中实现绘图非常简单,大致流程如下:

1. 创建一个canvas,并设置大小

2. 取得canvas的context

3. 加载图片

4. 通过context绘制图片

实现的代码:

// 创建canvas,并初始化(我们也可以直接以标签形式写在页面中,然后通过id等方式取得canvas)

var canvas=document.createElement(“canvas”);

canvas.width=600;

canvas.height=400;

document.body.appendChild(canvas);

// 取得2d绘图上下文

var context= canvas.getContext(“2d”);

// 加载图片,加载后在context上进行绘制。(图片是异步加载,所以在onload事件里进行绘制)

var image = new Image();

image.src = ”./res/bg。png”;

image.onload=function(event){

var loadedImg=event.target;

// 将加载后的图片,绘制在画布坐标[dx,dy]处,也就是图片的左上角坐标为[dx,dy]

var dx=0,dy=0 ;

context.drawImage(loadedImg,dx,dy);

};

上面的例子只是加载并绘制了一张图片。 而开发游戏时,我们往往会需要多张的图片。

为了更好的加载并管理这些图片,通常需要编写一个简单的资源加载的工具函数。

引入该函数后的代码如下:

//一个简单的图片加载函数

function loadImage(srcList,callback){

/* 代码略,详见 /step1/step1-2.html 内的loadImage函数 */

}

ImgCache=loadImage( [

{  id : "player",

url : "../res/player.png"

},

{  id : "bg",

url : "../res/bg.png"

}

],

startDemo );

function startDemo(){

// 绘制背景

var dx=0, dy=0 ;

context.drawImage(ImgCache["bg"],dx,dy);

//绘制站在地上的player,坐标为200,284

var sx=0, sy=60, sw=50, sh=60;

var dx=400, dy=284, dw=50, dh=60;

context.drawImage(ImgCache["player"], sx, sy, sw, sh, dx, dy, dw, dh );

}

实现上述需求的代码如下

// 一些简单的初始化,

var FPS=30;

var sleep=Math.floor(1000/FPS);

var img=ImgCache["player"];

//初始坐标

var x=0, y=284;

//移动速度speedY<0,向上移动。

var speedX = 65/1000 , speedY=-45/1000 ;

//x/y坐标的最大值和最小值,可用来限定移动范围。

var minX=0, maxX=500, minY=0, maxY=284;

//主循环

var mainLoop=setInterval(function(){

//距上一次执行相隔的时间。(时间变化量),目前可近似看作sleep。

var deltaTime=sleep;

//每次循环,改变一下绘制的坐标.

x=x+speedX*deltaTime; //向右移动

y=y+speedY*deltaTime; //向上移动,,坐标y减小,这点和数学中的坐标系不同.

//限定移动范围

x=Math.max(minX,Math.min(x,maxX));

y=Math.max(minY,Math.min(y,maxY));

//使用清空画布的方式,清空之前绘制的图片

//context.clearRect(0,0,canvas.width,canvas.height);

//使用背景覆盖的方式,清空之前绘制的图片

context.drawImage(ImgCache["bg"],0,0);

//在新位置上绘制图片

var sx=0, sy=60, sw=50, sh=60;

context.drawImage(img, sx, sy, sw, sh, Math.floor(x), Math.floor(y), sw, sh );

},sleep);

要实现一个Animation,首先要定义Frame。 看一个简单的示例:

var frame={

img : ImgCache["player"] ,

x : 0,

y : 60,

w : 50,

h : 60,

duration : 100

}

其中img属性 ,就是这帧要显示的图片—-之前已经加入到ImgCache中的那张马里奥的“帧集合”图片。一个Frame只对应这张图片中的一个区域,接下来的4个属性定义了该区域:X,y 区域左上角坐标。 W,h区域的宽度和高度。duration属性定义了该帧在播放时显示的时间,单位毫秒。上面的定义的帧 最后对应的图像就是下图中蓝框中的部分。

把若干个这样的frame放在一起,然后依次进行更新和绘制 就可以产生动画。一个Animation对象的结构大致如下:

var animation = {

frames : null ,

frameCount : -1 ,

currentFrame : null ,

currentFrameIndex : -1 ,

currentFramePlayed : -1 ,

img : ImgCache["player"]

}

不难发现,这和第二节中,移动图片时的流程非常相像。为了更好的实现这个流程, 也为后面更复杂的场景打下一个坚实的技术,我们可以引入面向对象的思想,对animation相关代码进行重构,封装出如下的Animation类。

// Animation类。

// cfg为Object类型的参数集, 其属性会覆盖Animation原型中定义的同名属性。

function Animation(cfg){

for (var attr in cfg ){

this[attr]=cfg[attr];

}

}

Animation。prototype={

constructor :Animation ,

// Animation 包含的Frame,类型:数组

frames : null,

// 包含的Frame数目

frameCount : -1 ,

// 所使用的图片id(在ImgCache中存放的Key), 字符串类型。

img : null,

currentFrame : null ,

currentFrameIndex : -1 ,

currentFramePlayed : -1 ,

// 初始化Animation

init : function(){

// 根据id取得Image对象

this。img = ImgCache[this。img]||this。img;

this。frames=this。frames||[];

this。frameCount = this。frames。length;

// 缺省从第0帧播放

This.setFrame(0);

},

//设置当前帧

setFrame : function(index){

this.currentFrameIndex=index;

this.currentFrame=this。frames[index];

this.currentFramePlayed=0;

},

// 更新Animation状态。 deltaTime表示时间的变化量。

update : function(deltaTime){

//判断当前Frame是否已经播放完成,

if (this。currentFramePlayed>=this。currentFrame。duration){

//播放下一帧

if (this.currentFrameIndex >= this。frameCount-1){

//当前是最后一帧,则播放第0帧

This.currentFrameIndex=0;

}else{

//播放下一帧

this。currentFrameIndex++;

}

//设置当前帧信息

This.setFrame(this。currentFrameIndex);

}else{

//增加当前帧的已播放时间。

This.currentFramePlayed += deltaTime;

}

},

//绘制Animation

draw : function(gc,x,y){

var f=this.currentFrame;

gc.drawImage (this.img, f.x , f.y, f.w, f.h , x, y, f.w, f.h );

}

};

其中稍微复杂点的是类的update方法。update方法中的 deltaTime 参数 表示距上一次执行update方法到这次执行所流逝的时间。在本例中,它约等于sleep ,所以在这里我们就使用 sleep。

实现动画效果的代码如下:

// 一些简单的初始化,

var FPS=30;

var sleep=Math.floor(1000/FPS);

//初始坐标

var x=0, y=284;

// 创建一个Animation对象

var animation = new Animation({

img : ”player” ,

//该动画由3帧构成,对应图片中的第一行。

frames : [

{x : 0, y : 0, w : 50, h : 60, duration : 100},

{x : 50, y : 0, w : 50, h : 60, duration : 100},

{x : 100, y : 0, w : 50, h : 60, duration : 100} ]

} );

// 初始化Animation

animation。init();

//主循环

var mainLoop=setInterval(function(){

//距上一次执行相隔的时间。(时间变化量), 目前可近似看作sleep。

var deltaTime=sleep;

// 更新Animation状态

Animation.update(deltaTime);

//使用背景覆盖的方式 清空之前绘制的图片

Context.drawImage(ImgCache["bg"],0,0);

//绘制Animation

Animation.draw(context, x,y);

},sleep);

运行这个示例 可以看到一个原地踏步的马里奥。下面我们结合移动图片的示例, 来让这个原地踏步的马里奥真正的走动起来。实现Anmation 和 移动的主循环可以合并成一个。 事实上,在游戏开发中通常也都是合并的。无论有多少个动画 游戏的主线程都只有一个。代码如下:

// 一些简单的初始化,

var FPS=30;

var sleep=Math.floor(1000/FPS);

//初始坐标

var x=0, y=284;

//移动速度。speedY<0,向上移动。

var speedX = 65/1000 , speedY=-45/1000 ;

//x/y坐标的最大值和最小值, 可用来限定移动范围。

var minX=0, maxX=500, minY=0, maxY=284;

// 创建一个Animation对象

var animation = new Animation({

img : ”player” ,

//该动画由3帧构成

frames : [

{x : 0, y : 0, w : 50, h : 60, duration : 100},

{x : 50, y : 0, w : 50, h : 60, duration : 100},

{x : 100, y : 0, w : 50, h : 60, duration : 100} ]

} );

// 初始化Animation

Animation.init();

//主循环

var mainLoop=setInterval(function(){

//距上一次执行相隔的时间。(时间变化量), 目前可近似看作sleep。

var deltaTime=sleep;

//每次循环,改变一下绘制的坐标。

x=x+speedX*deltaTime; //向右移动

y=y+speedY*deltaTime; //向上移动, 坐标y减小,这点和数学中的坐标系不同。

//限定移动范围

x=Math.max(minX,Math.min(x,maxX));

y=Math.max(minY,Math.min(y,maxY));

// 更新Animation状态

Animation.update(deltaTime);

//使用背景覆盖的方式,清空之前绘制的图片

Context.drawImage(ImgCache["bg"],0,0);

//绘制Animation

animation。draw(context, x,y);

},sleep);

显然,把一个人物的各种动作和属性,放到一个对象里,进行统一的管理,是一个不错的主意。而这个对象,就是我们常说的精灵(Sprite)。 下面我们引入一些面向对象的思想来编写一个Sprite类:

function Sprite(cfg){

for (var attr in cfg){

this[attr]=cfg[attr];

}

}

Sprite.prototype={

constructor :Sprite ,

//精灵的坐标

x : 0,

y : 0,

//精灵的速度

speedX : 0,

speedY : 0,

//精灵的坐标区间

minX : 0,

maxX : 9999,

minY : 0,

maxY : 9999,

//精灵包含的所有 Animation 集合. Object类型, 数据存放方式为” id : animation ”.

anims : null,

//默认的Animation的Id , string类型

defaultAnimId : null,

//当前的Animation.

currentAnim : null,

//初始化方法

init : function(){

//初始化所有Animtion

for (var animId in this.anims){

var anim=this.anims[animId];

anim.id=animId;

anim.init();

}

//设置当前Animation

this.setAnim(this.defaultAnimId);

},

//设置当前Animation, 参数为Animation的id, String类型

setAnim : function(animId){

this.currentAnim=this.anims[animId];

//重置Animation状态(设置为第0帧)

this.currentAnim.setFrame(0);

},

// 更新精灵当前状态。

update : function(deltaTime){

//每次循环,改变一下绘制的坐标。

this.x=this.x+this.speedX*deltaTime; //向右移动

this.y=this.y+this.speedY*deltaTime; //向上移动,坐标y减小,这点和数学中的坐标系不同。

//限定移动范围

this.x=Math.max(this.minX,Math.min(this.x,this.maxX));

this.y=Math.max(this.minY,Math.min(this.y,this.maxY));

if (this.currentAnim){

this.currentAnim.update(deltaTime);

}

},

//绘制精灵

draw : function(gc){

if (this.currentAnim){

this.currentAnim.draw(gc, this.x, this.y);

}

}

};

一个Sprite对象可以是游戏里的一个人物,也可以是一个道具(门、食物),也可以是一颗子弹、一个景物。具体把把游戏中的哪些物体定义成Sprite,要视游戏而定。在本文示例中, 主角马里奥,以及敌人都定义为Sprite。前面定义好了Sprite类, 下面看一下如何创建我们的主角马里奥。

var sprite = new Sprite({

//初始坐标

x : 0,

y : 284,

//移动速度. speedY=0,垂直方向不移动.

speedX : 90/1000,

speedY : 0,

//x/y坐标的最大值和最小值, 可用来限定移动范围.

minX : 0,

maxX : 500,

minY : 0,

maxY : 284,

defaultAnimId : ”walk-right”,

//定义两个Animation,向左走 和 向右走.

anims : {

“walk-left” : new Animation({

img : ”player” ,

frames : [

{x : 0, y : 60, w : 50, h : 60, duration : 100},

{x : 50, y : 60, w : 50, h : 60, duration : 100},

{x : 100, y : 60, w : 50, h : 60, duration : 100}

]

} ),

“walk-right” : new Animation({

img : ”player” ,

frames : [

{x : 0, y : 0, w : 50, h : 60, duration : 100},

{x : 50, y : 0, w : 50, h : 60, duration : 100},

{x : 100, y : 0, w : 50, h : 60, duration : 100}

]

} )

}

});

前面曾用来表示Animation位置和运动方式的几个变量:x、y、speedX、speedY等,因为都是马里奥的基本属性,所以都设置到Sprite对象上。这个Sprite由两个Animation构成,id分别为 walk-left 和 walk-right。 想要显示哪个动画,就执行 sprite.setAnim(“动画id”);

下面我们改变一下动画的逻辑:马里奥在画面上左右来回走动(垂直方向不动),那么动画的主循环变为:

// 定义sprite走路速度的绝对值,和默认的speedX

Sprite.walkSpeed=90/1000;

Sprite.speedX=sprite.walkSpeed;

// 初始化sprite

Sprite.init();

//主循环

var mainLoop=setInterval(function(){

//距上一次执行相隔的时间。(时间变化量),目前可近似看作sleep。

var deltaTime=sleep;

// 更新sprite状态

Sprite.update(deltaTime);

//如果做到最右侧,则折向左走,如果走到最左侧,则向右走。

//通过改变speedX的正负,来改变移动的方向。

if (sprite.x>=sprite.maxX){

sprite.speedX=-sprite.walkSpeed;

sprite.setAnim(“walk-left”);

}else if (sprite.x<=sprite.minX){

Sprite.speedX=sprite.walkSpeed;

Sprite.setAnim(“walk-right”);

}

//使用背景覆盖的方式 清空之前绘制的图片

Context.drawImage(ImgCache["bg"],0.0);

//绘制sprite

Sprite.draw(context);

},sleep);

现在代码变得越来越多, 为了便于维护和阅读, 从本节开始将js代码分文件存放。

输入与控制

一个可以走来走去的马里奥,显然不能称作是一款游戏, 这里就来说一说,如何实现玩家对游戏角色的控制。对于通过键盘操控的游戏,我们可以通过监听浏览器的keydown/keyup事件来进行检测。代码如下:

var Key={

A : 65,

W : 87,

D : 68

}

//用来记录按键状态

var KeyState={};

function initEvent(){

//监听整个document的keydown,keyup事件,为了保证能够监听到,监听方式使用Capture

document.addEventListener(“keydown”,function(evt){

//按下某按键,该键状态为true

KeyState[evt.keyCode]=true;

},true);

document.addEventListener(“keyup”,function(evt){

//放开下某按键,该键状态为true

KeyState[evt.keyCode]=false;

},true);

}

通过上面的代码记录按键状态后,就可以在游戏的主循环里根据不同按键状态来执行不同的操作。

前面,我们通过判断马里奥的位置来改变运动方向和Animation,下面来看一看如何通过键盘来控制马里奥左右移动以及跳跃。左右方向是匀速直线运动,所以的原理和前述的类似, 通过改变speedX的正负来改变方向。但是跳跃相对复杂些,跳跃在垂直方向上属于一种上抛运动, 要引入起跳初速度和重力加速度。由于处理玩家输入以及改变马里奥运动状态的代码比较多,且有相关性,所以给马里奥的精灵增加一个handleInput方法:

handleInput : function(){

// 读取按键状态,如果A键为按下状态,则向左移动,如果D键为按下状态,则向右移动。

var left= KeyState[Key.A];

var right= KeyState[Key.D];

var up= KeyState[Key.W];

//取得人物当前面对的方向

var dirX=this.currentAnim.id.split(“-”)[1];

// 判断是否落地

if (this.y==this.maxY){

this.jumping=false;

this.speedY=0;

}

//如果按了上,且当前不是跳跃中,那么开始跳跃。跳跃和走路使用同一个Animation。

if (up && !this.jumping){

this.jumping=true;

this.speedY=this.jumpSpeed;

this.setAnim(“walk-”+dirX);

}

if (left && right || !left && !right){

// 如果左右都没有按或者都按了,那么水平方向速度为0,不移动

this.speedX=0;

//如果不是在跳跃中,那么进入站立状态,站立时面对的方向根据之前的速度来决定

if (!this.jumping){

this.setAnim(“stand-”+dirX);

}

}else if(left && this.speedX!=-this.walkSpeed){

//如果按下了左,且当前不是向左走,则设置为向左走

this.setAnim(“walk-left”);

this.speedX=-sprite.walkSpeed;

}else if(right && this.speedX!=this.walkSpeed){

//如果按下了右,且当前不是向右走,则设置为向右走

this.setAnim(“walk-right”);

this.speedX=sprite.walkSpeed;

}

}

这个handleInput方法还是比较好理解的,但是何时调用它是一个值得注意的问题。我们通过按键,改变的是移动的速度,而不是直接改变坐标。 如果要让改变的速度生效,这个速度必须要持续一段时间。而这一段时间通常就是主循环两次迭代之间的间隔。也就是说,我们第n次迭代时改变的速度,要让它在第n+1次生效才有意义。

所以我们在迭代的最后调用 handleInput 方法。

// 定义走路速度的绝对值, 默认speedX

Sprite.walkSpeed=200/1000;

Sprite.speedX=0;

//定义跳跃初速度,垂直加速度, 默认speedY

Sprite.jumpSpeed=-700/1000;

Sprite.acceY=1。0/1000;

Sprite.speedY=0;

//默认情况下向右站立。

Sprite.defaultAnimId=”stand-right”;

// 初始化sprite

Sprite.init();

//主循环

var mainLoop=setInterval(function(){

//距上一次执行相隔的时间。(时间变化量), 目前可近似看作sleep。

var deltaTime=sleep;

// 更新sprite状态

sprite。update(deltaTime);

//使用背景覆盖的方式 清空之前绘制的图片

Context.drawImage(ImgCache["bg"],0,0);

//绘制sprite

Sprite.draw(context);

//处理输入,当前输入,影响下一次迭代。

Sprite.handleInput();

},sleep);

游戏主控类

本文示例中,除了主角马里奥之外,还有5个来回移动的敌人。他们也是精灵。随着精灵的增加和代码的进一步复杂话,我们需要再次重构,封装出游戏总的控制类:Game 。Game类中包含 多个Sprite,在每次迭代时,由Game负责所有精灵的状态更新和绘制。同时游戏的canvas context、全局的基本属性(如宽高、FPS)、启动函数、主循环等也都包含在Game里。代码如下:

有了这个主控类之后,创建游戏的流程如下:

1. 创建Animation对象;

2. 创建Sprite对象,并加入相应的Animation;

3. 创建Game对象,并把Sprite加入Game;

4. 初始化Game对象,同时初始化内部的Sprite,以及Sprite内的Animation;

5. 开始游戏。

当游戏中 产生新的精灵(如出现新敌人), 旧的精灵消失(被消灭的敌人)时, 只要对 Game中的sprites集合进行操作即可。

下面看一下重构后的启动代码,简单了许多:

//声明全局game对象

var game;

// Demo的启动函数

function startDemo(){

//创建game对象

game=new Game({

FPS : 30,

width : 600,

height : 400,

sprites : [ ]

});

//将必要的精灵加入game的精灵列表里

//加入马里奥

game.sprites.push(createPlayer());

//初始化game

game.init();

//开始game

game.start();

}

}

敌人与碰撞检测

有了主控类,向游戏中添加敌人就变得容易了许多。下面试着向游戏中加入5个敌人,由于敌人的外观和属性基本相同,不同的就是初始的坐标,和移动的速度,所以可以将创建敌人的过程封装成一个函数。

function createEnemy(){

var r=genRandom(0,1);

var cfg = {

img : ”enemy”,

x : r?500:0,

y : 294,

//x/y坐标的最大值和最小值,可用来限定移动范围。

minX : 0,

maxX : 500,

minY : 0,

maxY : 294,

handleInput : function(){

var s=genRandom(-4,4);

var moveSpeed=(150+s*10)/1000;

this.speedX=this.speedX||moveSpeed;

if (this.x<=this.minX){

this.x=this.minX;

this.speedX= moveSpeed;

}else if (this.x>=this.maxX){

this.x=this.maxX;

this.speedX=-moveSpeed;

}

},

defaultAnimId : ”move”,

anims : {

“move” : new Animation({

img : ”enemy” ,

frames : [

{x : 0, y : 0, w : 50, h : 50, duration : 100 },

{x : 50, y : 0, w : 50, h : 50, duration : 100  },

{x : 100, y : 0, w : 50, h : 50, duration : 100  },

{x : 150, y : 0, w : 50, h : 50, duration : 100  }

]

})

}

};

return new Sprite(cfg) ;

}

在游戏初始化之前,将精灵加入到Game主控类中:

//将必要的精灵加入game的精灵列表里

//加入马里奥

game.sprites.push(createPlayer());

//加入五个敌人

for(var i=0;i<5;i++){

game.sprites.push(createEnemy());

}

//初始化game

game.init();

//开始game

game.start();

现在运行游戏后,就可以看到一个马里奥和一群来回移动的敌人了。有了主角和敌人,下一步要做的就是对两者进行碰撞检测, 判断主角和敌人是否有接触,如果接触到了便Game Over。对于简单的2D游戏,通常只需取Sprite当前Frame的外框,作为碰撞区域,进行简单的矩形碰撞检测即可。

当上图中红色和蓝色矩形有交集时,则认为马里奥和敌人它们发生了,检测函数如下:

//返回true为两矩形发生碰撞。

checkIntersect : function(rect1, rect2){

return !(rect1.x1>rect2.x2 || rect1.y1>rect2.y2 || rect1.x2<rect2.x1 ||  rect1.y2<rect2.y1);

}

其中x1,y1为矩形左上角坐标, x2,y2为矩形右下角坐标。

下面为Sprite类添加两个方法:

//取得精灵的碰撞区域,

getCollRect : function(){

if (this.currentAnim){

var f=this.currentAnim.currentFrame;

return {

x1 : this.x,

y1 : this.y,

x2 : this.x+f.w,

y2 : this.y+f.h

}

}

},

//判断是否和另外一个精灵碰撞

collideWidthOther : function(sprite2){

var rect1=this.getCollideRect();

var rect2=sprite2.getCollideRect();

return rect1 && rect2 && !(rect1.x1>rect2.x2 || rect1.y1>rect2.y2 || rect1.x2<rect2.x1 ||  rect1.y2<rect2.y1);

}

现在我们就可以使用emeny.collideWidthOther(player)来判断敌人是否和主角发生了碰撞, 如果碰撞了就提示“Game Over”。

为Game类加入碰撞检方法:

//碰撞检测,返回true 则发生了主角和敌人的碰撞。

checkCollide : function(){

//本示例中,主角为第一个精灵。

var player=this.sprites[0];

for (var i=1,len=this.sprites.length;i<len;i++){

var sprite=this.sprites[i];

var coll=sprite.collideWidthOther(player);

if (coll){

return coll;

}

}

return false;

},

在主循环那恰当的位置进行碰撞检测:

//主循环中要执行的操作

run : function(deltaTime){

//碰撞检测

var coll=this.checkCollide();

if (coll){

//如果发生敌人和玩家的碰撞,则结束游戏。

clearInterval(this.mainLoop);

alert(“Game Over”);

return;

}

this.update(deltaTime);

this.clear(deltaTime);

this.draw(deltaTime);

//处理输入,当前输入,影响下一次迭代。

this.handleInput();

},

现在我们就可以通过键盘上的A/D/W键来控制人物进行移动和躲避敌人了。(要先关闭输入法)。

记录分数

现在,已经完成了游戏核心部分的开发,下面加入对分数的记录和显示,这里的分数就是在游戏中坚持的时间。首先在页面中加入一个简单的浮动div, 用来显示分数。

<div id=”statebar” style=”font-size:24px; position:absolute; top:10px; left:280px;” >

Time : <span id=”timeCount”></span>

</div>

然后在游戏主循环中,更新这个div的显示。修改Game类的start 和run方法如下:

start : function(){

var Me=this;

// 记录游戏开始时间

Me.startTime=Date.now();

//主循环

this.mainLoop=setInterval(function(){

//距上一次执行相隔的时间(时间变化量),目前可近似看作sleep。

var deltaTime=Me.sleep;

Me.run(deltaTime);

},Me.sleep);

},

//主循环中要执行的操作

run : function(deltaTime){

//显示游戏时间

var palyedTime = Date.now()-this.startTime;

$(“timeCount”).innerHTML=palyedTime;

//碰撞检测

var coll=this.checkCollide();

if (coll){

//如果发生敌人和玩家的碰撞,则结束游戏.

clearInterval(this.mainLoop);

alert(“Game OVer.\n Your score : ”+palyedTime);

return;

}

this.update(deltaTime);

this.clear(deltaTime);

this.draw(deltaTime);

//处理输入,当前输入,影响下一次迭代。

this.handleInput();

},

更多代码我们将陆续整理出来供大家参考,谢谢关注。

2011年4期《程序员》配套源码及相关链接

转播到腾讯微博

----->立刻申请加入《程序员》杂志读者俱乐部,与杂志编辑直接交流,参与选题,优先投稿

8 Responses to “2011年4期《程序员》配套源码及相关链接”

  1. lohu 说道:

    很详细,收藏了

  2. bandit 说道:

    很多好东东

  3. abc881858 说道:

    不错的资源 O(∩_∩)O谢谢

  4. buy north face 说道:

    buy north face千里马常有,而伯乐不常有

  5. cheap north face 说道:

    员工的积极性可以调动起来呀

  6. sam 说道:

    很强大!!

  7. lu 说道:

    说得好…….

请评论

preload preload preload
京ICP备06065162