12.26 HTML Canvas动画 - 游戏原理

如何使用Canvas来进行游戏编程

"层"的概念

一个游戏里面包含了许多个元素,包括图片、声音、对象模型、相关数据(比如"游戏得分")、方向行为控制等等。游戏编程的实质就是把这些元素按照剧本有序的组合在一起,把这些元素“可控“化。

跟影视制作一样,游戏的每一个场景里面都有"背景"、"主角"、"配角"、"路人甲乙丙丁"、"道具"等。在影视拍摄中,导演指挥剧务布置场景,指挥演员按剧本表演动作和对话,后期制作中加入各种道具使用时产生的特效。在游戏制作中,游戏策划案就好比影视拍摄的剧本,而程序员就好比导演,只是我们用编程的方式来实现场景的布置;实现指挥和协同各个角色进行有规则的运转;实现使用各种道具(或处于不同情形)时的各种效果。

游戏中的场景大多由一张或多张"背景图"、N个"路人"对象、N个"配角”对象和一个"主角"对象以及相关的背景音乐、音效、选项按钮和控制按钮等组成,这些基本元素我们都称之为"资源"。这些"资源"可以直接以程序对象的形式存在,也可以是由资源文件在导入后以"数据对象"的形式存在。"资源"可复制,可被多次重复使用。"背景图"通常放置在最底层(z轴的最后面,见图1),因其后面已无其它任何图像存在,所以无须使用透明底来避免遮挡。如果使用的背景是以文件形式存在的,我们从文件大小的角度考虑,尽可能使用数据量小的格式(比如压缩率较高的.JPG格式等);"主角"、"配角"、和"路人"通常拥有不规则的边缘,工作时可能也会有不同角度的移动或尺寸的变化,但图像文件通常以矩形的外观存在,如果在不使用透明底的情况下会造成一些叠层遮挡。所以这里大多使用支付透明底的图像格式(比如.PNG和.GIF)。当然,直接使用程序对象或使用SVG矢量对象(SVG可以以文件方式独立存在)则更加有利于解决不同尺寸下图像清晰度的问题。

图1
”背景、路人、主角、配角“元素工作层位置

我们拿《用CANVAS做的接香蕉游戏》举例说明(见图2):
//wow.techbrood.com/fiddle/17180

图2
《用CANVAS做的接香蕉游戏》游戏中画面(左) 游戏结束画面(右)

图中绿色底是"背景"(森林的背景色),褐色的那些长方形是"路人"甲乙丙丁 (森林中的参天大树),灰色的小人是"主角",蓝色的长颈鹿、黄色的香蕉、红色的炸弹都是"配角"。"主角"和"配角"的方向移动控制属"方向控制功能","主角"和"配角"接触后的效果属"碰撞效果功能",图右上方的"暂停按钮"和游戏结束画面中间黄色的"Replay"重玩按钮属于行为"行为控制功能",图左下方和右下方的数字显示属于"相关数据"显示。此处可见,"背景层"相对来说是以静态为主(除非按需改变场景背景);"路人层"可以是静态也可以是动态的,比如图上树木是静止的,所以现在它是静态的;但如果上面要增加一些漂浮的云或正在树间穿越的猴子,那也可以为些单独的对象增加动画效果。总的来说,"背景层"和"路人层"不涉及主配角的操控互动,与图1上的"主、配角表演层"无直接关联。

用CSS来实现"背景层"的布置

如果背景是纯色彩,可以改变background值来改变背景色;如果要放置图片,则可以用background-image, background-size, background-repeat, bakground-position来布置相应大小的背景图案

canvas {
	background:	#197329;	//CANVAS元素的背景色,在这里就是游戏的绿色背景了
	margin:		30px auto;
}
	

游戏中的"对象"

任何可归类的"物"可视为是"对象"。每个"对象"都拥有自身不同的属性,比如类型(type),X/Y/Z轴位置,宽度、高度、厚度等等。使用"对象"的好处是我们可以使用统一的方法来对各个对象进行动态管理,包括取值、赋值以及更新等操作。

// 游戏对象模板
function gameObject(options) {
    this.type = options.type;	// 对象的类型
    this.x = options.x || 0;	// 对象的X轴位置
    this.y = options.y || 0;	// 对象的Y轴位置
    this.w = options.w || 0;	// 对象的宽度
    this.h = options.h || 0;	// 对象的高度
    this.vX = options.vX || 0;	// 对象X轴运动的加速度
    this.vY = options.vY || 0;	// 对象Y轴运动的加速度
    this.life = options.life || 0; // 对象是否"活着"
    this.damage = options.damage || 0;		// 对象是否"损坏"
    this.points = options.points || 0;	// 对象的点数
    this.move = options.move || function() {}; // 对象移动调用函数
    this.draw = options.draw || function() {};	// 对象绘制调用函数
}

// 按钮对象的模板
function clickBox(options) {
    this.x = options.x;	// 按钮对象的X轴位置
    this.y = options.y;	// 按钮对象的Y轴位置
    this.w = options.w;	// 按钮对象的宽度
    this.h = options.h;	// 按钮对象的高度
    this.active = options.active;	// 按钮对象是否活动
    this.draw = options.draw || function() {};	// 按钮对象绘制调用函数
}
	

游戏状态

游戏初始状态通常为“等待开始”(画面为游戏封面);在触发开始后游戏状态进入"运行"状态;在"暂停"触发后游戏状态改为"暂停";在符合游戏结束条件时游戏状态改为"结束"(画面为游戏结束画面,通常在游戏结束画面中有触发"重新开始"的按钮)。

游戏的得分和主角生命值

大多游戏使用"得分"来体现主角的成就,也用"生命值”用来判断是否结束游戏。所以这两个变量应该是全局变量,并需要在相应的事件中动态修改赋值。

用JS来绘制游戏封面

// 游戏封面的初始化函数
function mainScreen() {
    startSplash(ctx);
    startBox.draw(ctx);
}

// 绘制开始游戏按钮图形
var startImg = function(ctx) {
    ctx.fillStyle = 'rgb(255,255,255)';
    ctx.beginPath();
    ctx.moveTo(this.x, this.y);
    ctx.lineTo(this.x - 20, this.y + this.h);
    ctx.lineTo(this.x + this.w - 20, this.y + this.h);
    ctx.lineTo(this.x + this.w, this.y);
    ctx.closePath();
    ctx.fill();

    ctx.fillStyle = 'rgb(0,0,0)';
    ctx.font = "italic 32px Impact, Helvetica, Ariel";
    ctx.fillText("START", this.x, this.y + this.h - 15);
};

// 绘制游戏封面的背景图文
var startSplash = function(ctx) {
    ctx.fillStyle = 'rgb(242,242,51)';
    ctx.beginPath();
    ctx.moveTo(380, 0);
    ctx.lineTo(80, 500);
    ctx.lineTo(160, 500);
    ctx.lineTo(460, 0);
    ctx.closePath();
    ctx.fill();

    ctx.beginPath();
    ctx.moveTo(490, 0);
    ctx.lineTo(190, 500);
    ctx.lineTo(200, 500);
    ctx.lineTo(510, 0);
    ctx.closePath();
    ctx.fill();

    ctx.fillStyle = 'rgb(255,255,255)';
    ctx.beginPath();
    ctx.moveTo(700, 105);
    ctx.lineTo(250, 105);
    ctx.lineTo(220, 178);
    ctx.lineTo(700, 178);
    ctx.closePath();
    ctx.fill();

    ctx.fillStyle = 'rgb(255,255,255)';
    ctx.font = "italic 80px Impact, Helvetica, Ariel";
    ctx.fillText("BANANA", 62, 100);

    ctx.fillStyle = 'rgb(0,0,0)';
    ctx.font = "italic 80px Impact, Helvetica, Ariel";
    ctx.fillText("DROP", 260, 172);

    ctx.fillStyle = 'rgb(255,255,255)';
    ctx.font = "18px Helvetica, Ariel";
    ctx.fillText("Catch the falling bananas", 370, 260);
    ctx.fillText("Dodge the bombs", 370, 290);
    ctx.fillText("Arrow keys move you left and right", 370, 320);
};

// 设定"开始游戏按钮"对象(包括位置座标、宽度、高度以及状态和绘制图形所用的函数)
var startBox = new clickBox({
    x: 500,
    y: 380,
    w: 100,
    h: 60,
    active: true,
    draw: startImg
});
	

用JS来绘制游戏结束画面

// 绘制游戏结束时的字样以及更新游戏状态的函数
function endScreen() {
    ctx.fillStyle = 'rgb(255,255,255)';
    ctx.font = "italic 80px Impact, Helvetica, Ariel";

    ctx.fillText("GAME OVER", cvs.width / 2 - 200, cvs.height / 2);

    replayBtn.active = true;	// 设定“重玩游戏”按钮的激活状态为可用
    replayBtn.draw(ctx);
}

// 绘制重新开始游戏按钮图形
var replayImg = function(ctx) {
    ctx.fillStyle = 'rgb(242,242,51)';
    ctx.beginPath();
    ctx.moveTo(this.x, this.y);
    ctx.lineTo(this.x - 20, this.y + this.h);
    ctx.lineTo(this.x + this.w - 20, this.y + this.h);
    ctx.lineTo(this.x + this.w, this.y);
    ctx.closePath();
    ctx.fill();

    ctx.font = "italic 32px Impact, Helvetica, Ariel";
    ctx.fillStyle = 'rgb(0,0,0)';
    ctx.fillText("Replay", this.x - 5, this.y + this.h - 7);
};

// 设定"重新开始游戏按钮"对象(包括位置座标、宽度、高度以及状态和绘制图形所用的函数)
var replayBtn = new clickBox({
    x: cvs.width / 2 - 50,
    y: cvs.height / 2 + 30,
    w: 110,
    h: 40,
    active: false,
    draw: replayImg
});
	

监听游戏封面和结束游戏画面中的按钮

cvs.addEventListener(
	'mousedown',
	function(evt) {
        var mousePos = getMousePos(cvs, evt);
        // 如果是按在"开始游戏按钮"上的,则开始游戏
        if (startBox.testClick(mousePos.x, mousePos.y)) {
            startBox.active = false;	// 开始按钮设定为不可用
            startGame();	//	调用开始游戏的初始化函数
        }
        // 如果是按在"重新开始游戏按钮"上的,则重新开始游戏
        if (replayBtn.testClick(mousePos.x, mousePos.y)) {
            startGame();	//	调用开始游戏的初始化函数
        }
	}
);

//	开始游戏的初始化函数
function startGame() {
    gWorld = new gameWorld(6);
    gWorld.w = cvs.width;
    gWorld.h = cvs.height;

    gWorld.state = 'RUNNING';	// 更改游戏状态至'运行'
    gWorld.addObject(makeGround(gWorld.w, gWorld.h));
    gWorld.addObject(makeCanopy(gWorld.w));
    gWorld.addObject(makeWalker(gWorld.w, gWorld.h));
    gWorld.addObject(makeWalker(gWorld.w, gWorld.h, true));
    gWorld.addObject(makePlayer());

    startBox.active = false;	// 开始按钮设定为不可用
    pauseBtn.active = true;	// 暂停按钮设定为可用
    touchRightBtn.active = true;	// 触碰右边按钮设定为可用
    touchLeftBtn.active = true;	// 触碰左边按钮设定为可用
    replayBtn.active = false;	// 重新开始游戏按钮设定为不可用

    gameLoop();
}

	

用JS来绘制"路人"层上面的各资源图案

// 用来绘制森林树木的函数
var drawForrest = function(ctx, w, h) {

    ctx.fillStyle = 'rgba(31,103,41,0.8)';
    ctx.fillRect(0, 0, w, 64);

    ctx.fillStyle = 'rgb(160,101,48)';
    ctx.fillRect(100, 0, 52, h);

    ctx.beginPath();
    ctx.moveTo(100, 112);
    ctx.lineTo(42, 0);
    ctx.lineTo(54, 0);
    ctx.lineTo(100, 92);
    ctx.closePath();
    ctx.fill();

    ctx.fillRect(212, 0, 46, h);
    ctx.fillRect(322, 0, 32, h);
    ctx.fillRect(470, 0, 52, h);

    ctx.fillStyle = 'rgba(160,101,48,0.5)';
    ctx.fillRect(52, 0, 16, h);
    ctx.fillRect(276, 0, 14, h);
    ctx.fillRect(412, 0, 18, h);
};

// 在游戏中生成实例化深褐色地面对象的函数
var makeGround = function(worldW, worldH) {
    var ground = new gameObject({
        type: 'ground',
        x: 0,
        y: worldH - 25,
        w: worldW,
        h: 25,
        life: 1,
        draw: drawGround
    });
    ground = isSolid(ground);
    return ground;
};

// 配合"对象"用来绘制深褐色地面的函数
var drawGround = function(ctx) {
    ctx.fillStyle = 'rgb(79,62,25)';
    ctx.fillRect(this.x, this.y, this.w, this.h);
};

// 在游戏中生成实例化绿色顶部操作栏的函数
var makeCanopy = function(worldW) {
    var canopy = new gameObject({
        x: 0,
        y: 0,
        w: worldW,
        h: 50,
        life: 1,
        draw: drawCanopy
    });
    return canopy;
};

// 配合"对象"用来绘制顶部绿色的操作栏的函数
var drawCanopy = function(ctx) {
    ctx.fillStyle = 'rgb(43,142,60)';
    ctx.fillRect(this.x, this.y, this.w, this.h);
};
	

用JS来绘制"主、配角表演层"上面的各资源图案(或动画效果)

// 在游戏中生成一个实例化香蕉对象的函数
var makeBanana = function(worldW) {
    var banana = new gameObject({
        type: 'banana',
        x: Math.random() * worldW,
        y: 0,
        w: 10,
        h: 18,
        life: 1,
        points: 1,
        move: fall,
        draw: drawBanana
    });
    banana = isSolid(banana);
    banana = isDestructable(banana);
    banana = givesPoints(banana);
    return banana;
};

// 配合"对象"来绘制单个香蕉对象 (图形和下落的动画效果)的函数
var drawBanana = function(ctx, frame) {
    ctx.fillStyle = 'rgb(255,255,51)';
    switch (frame) {
        case 0:
            ctx.fillRect(this.x, this.y + this.h * 0.6, this.w, this.h * 0.4);
            break;
        case 1:
            ctx.beginPath();
            ctx.moveTo(this.x, this.y);
            ctx.lineTo(this.x, this.y + this.h / 2);
            ctx.lineTo(this.x + this.w, this.y + this.h);
            ctx.lineTo(this.x + this.w * 0.4, this.y + this.h / 2);
            ctx.lineTo(this.x, this.y);
            ctx.fill();
            break;
    }
};

// 在游戏中生成一个实例化苹果(炸弹)对象的函数
var makeApple = function(worldW) {
    var apple = new gameObject({
        type: 'apple',
        x: Math.random() * worldW,
        y: 0,
        w: 10,
        h: 18,
        life: 1,
        damage: 1,
        move: fall,
        draw: drawApple
    });
    apple = isSolid(apple);
    apple = doesDamage(apple);
    apple = isDestructable(apple);
    return apple;
};

// 配合"对象"来绘制单个苹果(炸弹)对象 (图形和下落的动画效果)的函数
var drawApple = function(ctx, frame) {
    ctx.fillStyle = 'rgb(235,32,57)';
    switch (frame) {
        case 0:
            ctx.beginPath();
            ctx.arc(this.x - this.w, this.y - this.w, this.w / 2, 0, Math.PI * 2, true);
            ctx.fill();

            ctx.beginPath();
            ctx.arc(this.x - this.w, this.y + this.w, this.w / 2, 0, Math.PI * 2, true);
            ctx.fill();

            ctx.beginPath();
            ctx.arc(this.x + this.w, this.y - this.w, this.w / 2, 0, Math.PI * 2, true);
            ctx.fill();

            ctx.beginPath();
            ctx.arc(this.x + this.w, this.y + this.w, this.w / 2, 0, Math.PI * 2, true);
            ctx.fill();
            break;
        case 1:
            ctx.fillStyle = 'rgb(235,32,57)';
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.w, 0, Math.PI * 2, true);
            ctx.fill();
            break;
    }
};

// 在游戏中生成两个实例化长颈鹿对象的函数
var makeWalker = function(worldW, worldH, rightWall) {
    var walker = new gameObject({
        type: 'walker',
        x: 0,
        y: 210,
        w: 130,
        h: 225,
        life: 1,
        draw: drawWalker
    });

    walker.startWalker = false;
    walker.maxX = 0;
    walker.mode = 'ADVANCE';
    walker.startSide = 'LEFT';

    if (rightWall) {
        walker.x = worldW;
        walker.startSide = 'RIGHT';
    } else {
        walker.x = 0 - walker.w;
    }
    walker.startX = walker.x;
    walker.move = walkerMove;

    walker.r1 = {
        mod: 0,
        dir: 'ADD',
        max: walker.w * .3,
        min: 0
    }

    walker.r2 = {
        mod: 0,
        dir: 'ADD',
        max: walker.w - walker.w * .7,
        min: 0
    }

    walker.l1 = {
        mod: walker.w * .3,
        dir: 'SUB',
        max: walker.w * .3,
        min: 0
    }

    walker.l2 = {
            mod: walker.w - walker.w * .7,
            dir: 'SUB',
            max: walker.w - walker.w * .7,
            min: 0
        }

    walker = isSolid(walker);
    return walker;
};

// 配合"对象"绘制长颈鹿 (图形和走动的动画效果)的函数
var drawWalker = function(ctx) {
	// 画右前腿
    ctx.fillStyle = 'rgb(8,79,150)';
    ctx.beginPath();
    ctx.moveTo(this.x + this.w * 0.1, this.y + this.h * 0.4);
    ctx.lineTo(this.x + this.l2.mod, this.y + this.h * 0.6);
    ctx.lineTo(this.x + this.l2.mod, this.y + this.h);
    ctx.lineTo(this.x + this.w * 0.1 + this.l2.mod, this.y + this.h * 0.6);
    ctx.lineTo(this.x + this.w * 0.5, this.y + this.h * 0.4);
    ctx.closePath();
    ctx.fill();

	// 画右后腿
    ctx.beginPath();
    ctx.moveTo(this.x + this.w * 0.5, this.y + this.h * 0.4);
    ctx.lineTo(this.x + this.w * 0.7 + this.l2.mod, this.y + this.h * 0.75);
    ctx.lineTo(this.x + this.w * 0.7 + this.l2.mod, this.y + this.h);
    ctx.lineTo(this.x + this.w * 0.8 + this.l2.mod, this.y + this.h * 0.75);
    ctx.lineTo(this.x + this.w * 0.8, this.y + this.h * 0.4);
    ctx.closePath();
    ctx.fill();

    // 画左前腿
    ctx.fillStyle = 'rgb(49,147,245)';
    ctx.beginPath();
    ctx.moveTo(this.x, this.y + this.h * 0.2);
    ctx.lineTo(this.x + this.w * 0.1, this.y + this.h * 0.15);
    ctx.lineTo(this.x + this.w * 0.1, this.y + this.h * 0.4); //to first leg
    ctx.lineTo(this.x + this.r1.mod, this.y + this.h * 0.6);
    ctx.lineTo(this.x + this.r1.mod, this.y + this.h);
    ctx.lineTo(this.x + this.w * 0.1 + this.r1.mod, this.y + this.h * 0.6);
    ctx.lineTo(this.x + this.w * 0.5, this.y + this.h * 0.45);

    // 画左后腿
    ctx.lineTo(this.x + this.w * 0.7 + this.r2.mod, this.y + this.h * 0.75);
    ctx.lineTo(this.x + this.w * 0.7 + this.r2.mod, this.y + this.h);
    ctx.lineTo(this.x + this.w * 0.8 + this.r2.mod, this.y + this.h * 0.75);

    // 画上半身
    ctx.lineTo(this.x + this.w * 0.8, this.y + this.h * 0.3);
    ctx.lineTo(this.x + this.w * 0.5, this.y + this.h * 0.2);
    ctx.lineTo(this.x + this.w * 0.4, this.y + this.h * 0.1);
    ctx.lineTo(this.x + this.w * 0.2, this.y);
    ctx.lineTo(this.x + this.w * 0.1, this.y + this.h * 0.1);
    ctx.closePath();
    ctx.fill();

    // 往左和往右走动时的动画效果,到达边界则改变方向
    if (this.r1.dir == 'ADD') {
        this.r1.mod += 0.5;
        if (this.r1.mod >= this.r1.max) {
            this.r1.dir = 'SUB';
        }
    } else {
        this.r1.mod -= 0.5;
        if (this.r1.mod <= this.r1.min) {
            this.r1.dir = 'ADD';
        }
    }

    if (this.r2.dir == 'ADD') {
        this.r2.mod += 0.5;
        if (this.r2.mod >= this.r2.max) {
            this.r2.dir = 'SUB';
        }
    } else {
        this.r2.mod -= 0.5;
        if (this.r2.mod <= this.r2.min) {
            this.r2.dir = 'ADD';
        }
    }

    if (this.l1.dir == 'ADD') {
        this.l1.mod += 0.5;
        if (this.l1.mod >= this.l1.max) {
            this.l1.dir = 'SUB';
        }
    } else {
        this.l1.mod -= 0.5;
        if (this.l1.mod <= this.l1.min) {
            this.l1.dir = 'ADD';
        }
    }

    if (this.l2.dir == 'ADD') {
        this.l2.mod += 0.5;
        if (this.l2.mod >= this.l2.max) {
            this.l2.dir = 'SUB';
        }
    } else {
        this.l2.mod -= 0.5;
        if (this.l2.mod <= this.l2.min) {
            this.l2.dir = 'ADD';
        }
    }
};

// 对"长颈鹿"移动的动画效果处理(包括移动到水平边界后折返)
var walkerMove = function(options) {
    var speed = .5;
    if (this.startWalker) {
        if (this.startSide == 'LEFT') {
            if (this.mode == 'ADVANCE') {
                this.vX = speed;
                if (this.x + this.vX >= this.maxX) {
                    this.mode = 'RETREAT';
                }
            }

            if (this.mode == 'RETREAT') {
                this.vX = speed * -1;
                if (this.x + this.vX <= this.startX) {
                    this.startWalker = false;
                }
            }
        }

        if (this.startSide == 'RIGHT') {
            if (this.mode == 'ADVANCE') {
                this.vX = speed * -1;
                if (this.x + this.vX <= this.maxX) {
                    this.mode = 'RETREAT';
                }
            }

            if (this.mode == 'RETREAT') {
                this.vX = speed;
                if (this.x + this.vX >= this.startX) {
                    this.startWalker = false;

                }
            }
        }

    } else {
        this.vX = 0;
        var moveProb = Math.random() * 1000;

        if (moveProb < 5) {
            this.startWalker = true;
            this.mode = 'ADVANCE';
            dist = Math.random() * 200 + (this.w * .5);
            if (this.startSide == 'LEFT') {
                this.maxX = this.x + dist;
            }
            if (this.startSide == 'RIGHT') {
                this.maxX = this.x - dist;
            }
        }
    }
};

// 在游戏中生成一个实例化主角对象的函数
var makePlayer = function() {
    var player = new gameObject({
        type: 'player',
        x: 200,
        y: 396,
        w: 21,
        h: 38,
        vX: 0,
        vY: 0,
        damage: 5,
        life: 5,
        draw: drawCatcher
    });
    player = isSolid(player);
    player = isDestructable(player);
    player = userControlled(player);
    player = isAnimated(player, 2, 1);
    return player;
};

// 配合"对象"来绘制主角(图形和左右走动的动画效果)的函数
var drawCatcher = function(ctx, frame) {
    var front;
    var back;

    switch (frame) {
        case 0:
            front = 'rgb(51,29,8)';
            back = 'rgb(24,15,4)';
            drawCatcherTop(ctx, back, this.x, this.y, this.w, this.h);
            drawCatcherLeftLeg(ctx, back, this.x, this.y, this.w, this.h);
            drawCatcherRightLeg(ctx, front, this.x, this.y, this.w, this.h);
            break;
        case 3:
            front = 'rgb(212,176,140)';
            back = 'rgb(176,126,76)';
            drawCatcherTop(ctx, back, this.x, this.y, this.w, this.h);
            drawCatcherLeftLeg(ctx, back, this.x, this.y, this.w, this.h);
            drawCatcherRightLeg(ctx, front, this.x, this.y, this.w, this.h);
            break;
        case 2:
            front = 'rgb(201,201,201)';
            back = 'rgb(140,140,140)';
            drawCatcherTop(ctx, front, this.x, this.y, this.w, this.h);
            drawCatcherRightLeg(ctx, back, this.x, this.y, this.w, this.h);
            drawCatcherLeftLeg(ctx, front, this.x, this.y, this.w, this.h);
            break;
        case 1:
            front = 'rgb(201,201,201)';
            back = 'rgb(140,140,140)';
            drawCatcherTop(ctx, front, this.x, this.y, this.w, this.h);
            drawCatcherLeftLeg(ctx, back, this.x, this.y, this.w, this.h);
            drawCatcherRightLeg(ctx, front, this.x, this.y, this.w, this.h);
            break;
    }
};

// 绘制主角时需要用到的函数,用于画主角的"头部"
function drawCatcherTop(ctx, fillColor, x, y, w, h) {
    ctx.fillStyle = fillColor;
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x, y + h * 0.5);
    ctx.lineTo(x + w * 0.33, y + h * 0.5);
    ctx.lineTo(x + w * 0.33, y + h * 0.33);
    ctx.lineTo(x + w, y + h * 0.18);
    ctx.lineTo(x + w * 0.3, y + h * 0.18);
    ctx.lineTo(x + w * 0.3, y + h * 0.14);
    ctx.lineTo(x + w * 0.33, y + h * 0.14);
    ctx.lineTo(x + w * 0.33, y);
    ctx.closePath();
    ctx.fill();
}

// 绘制主角时需要用到的函数,用于画主角的"左腿"
function drawCatcherLeftLeg(ctx, fillColor, x, y, w, h) {
    ctx.fillStyle = fillColor;
    ctx.beginPath();
    ctx.moveTo(x, y + h * 0.5);
    ctx.lineTo(x, y + h * 0.7);
    ctx.lineTo(x - w * 0.33, y + h);
    ctx.lineTo(x + w * 0.33, y + h * 0.7);
    ctx.lineTo(x + w * 0.33, y + h * 0.5);
    ctx.closePath();
    ctx.fill();
}

// 绘制主角时需要用到的函数,用于画主角的"右腿"
function drawCatcherRightLeg(ctx, fillColor, x, y, w, h) {
    ctx.fillStyle = fillColor;
    ctx.beginPath();
    ctx.moveTo(x, y + h * 0.5);
    ctx.lineTo(x, y + h * 0.6);
    ctx.lineTo(x + w * 0.38, y + h * 0.74);
    ctx.lineTo(x + w * 0.66, y + h);
    ctx.lineTo(x + w * 0.66, y + h * 0.7);
    ctx.lineTo(x + w * 0.33, y + h * 0.5);
    ctx.closePath();
    ctx.fill();
}
	

主角的操作控制

通过监测左右光标键的按下和弹上来设定主角是否移动,是向左还是向右移动

// 在浏览器窗口中增加对键盘按下按钮的监测
window.addEventListener('keydown', function(e) {
    //光标右键操作主角向右,光标左键操作主角向左
    if (e.keyCode === 39) {
        gWorld.keyPressed.right = true;
    } else if (e.keyCode === 37) {
        gWorld.keyPressed.left = true;
    }

});

// 在浏览器窗口中增加对键盘按键跳上来后的监测
window.addEventListener('keyup', function(e) {
    //通过监测左右光标键是否弹上来(未按状态)来停止向左或向右移动
    if (e.keyCode === 39) {
        gWorld.keyPressed.right = false;
    } else if (e.keyCode === 37) {
        gWorld.keyPressed.left = false;
    }
});
	

主角和其它"对象"间的碰撞处理

在动画的每一桢中都需要更新所有对象的"属性",包括位置以及跟其它"对象"发生碰撞后应进行的一些属性改变。检测有否发生碰撞是通过计算两个对象座标间的距离来实现。本游戏的剧本中设定当香蕉或苹果跟主角发生碰撞后这些对象立即消失,如果发生碰撞的对象是香蕉则游戏加分;如果发生碰撞的是苹果则主角生命值下降。处理的方法就是当检测到碰撞后查看对象属性来识别类型,并对相应的得分和分命值变量做出相应的修改数值并更改当前对象的属于为”死亡“,这样这个对象在下一桢中将不被显示。

// 计算两个座标间距离的函数
function calculateDistance(x1, y1, x2, y2) {
    x = Math.abs(x1 - x2);
    y = Math.abs(y1 - y2);
    return Math.sqrt((x * x) + (y * y));
}

// 检测两个对象是否有发生碰撞的函数
function collisionDetect(obj1, obj2) {
    var left1;
    var right1;
    var top1;
    var bottom1;

    var left2;
    var right2;
    var top2;
    var bottom2;

    left1 = obj1.x;
    left2 = obj2.x;
    right1 = obj1.x + obj1.w;
    right2 = obj2.x + obj2.w;
    top1 = obj1.y;
    top2 = obj2.y;
    bottom1 = obj1.y + obj1.h;
    bottom2 = obj2.y + obj2.h;

    if (bottom1 < top2) return 0;
    if (top1 > bottom2) return 0;

    if (right1 < left2) return 0;
    if (left1 > right2) return 0;

    return 1;
}

// 更新游戏环境中的所有对象
gameWorld.prototype.updateGameObjects = function() {
    for (var i = 0; i < this.gameObjects.length; i++) {
        //让各个对象动起来
        this.gameObjects[i].move({
            gravity: this.gravity,
            keyPressed: this.keyPressed
        });

        //更新对象的最新位置
        this.gameObjects[i].x += this.gameObjects[i].vX;
        this.gameObjects[i].y += this.gameObjects[i].vY;

        //检测当前对象有没有碰撞到其它对象
        if (this.gameObjects[i].solid) {

            for (var l = 0; l < this.gameObjects.length; l++) {

                if (l != i && this.gameObjects[l].solid) {

                    if (collisionDetect(this.gameObjects[i], this.gameObjects[l])) {

                        if (this.gameObjects[i].giveDamage && this.gameObjects[l].destructable) {
                            this.gameObjects[l].life -= this.gameObjects[i].damage;		//如果发生碰撞的对象一方是主角,另一方是危险值高的"苹果“(炸弹),则主角生命值直接减去这个危险对象所赋的危险值
                        }

                        if (this.gameObjects[i].destructable && this.gameObjects[i].type != 'player') {
                            this.gameObjects[i].life -= 1;	  //如果发生碰撞的对象一方是主角,另一方是"苹果“(炸弹),则主角生命值减1分
                        }

                        if (this.gameObjects[i].givePoint && this.gameObjects[l].type == 'player') {
                            this.score += this.gameObjects[i].points;  //如果发生碰撞的对象一方是主角,另一方是"香蕉“,则游戏得分加1分
                        }

                        if (this.gameObjects[i].type == 'player') {
                            if (this.gameObjects[i].vX > 0) {
                                this.gameObjects[i].x = this.gameObjects[l].x - this.gameObjects[i].w;
                            }

                            if (this.gameObjects[i].vX < 0) {
                                this.gameObjects[i].x = this.gameObjects[l].x + this.gameObjects[l].w;
                            }
                        }

                        if (this.gameObjects[i].type == 'walker' && this.gameObjects[l].type == 'player') {
                            if (this.gameObjects[i].vX > 0) {
                                this.gameObjects[l].x = this.gameObjects[i].x + this.gameObjects[i].w;
                            }

                            if (this.gameObjects[i].vX < 0) {
                                this.gameObjects[l].x = this.gameObjects[i].x - this.gameObjects[l].w;
                            }
                        }
                    }
                }
            }
        }
    }
};

// 移除游戏环境中已死(无用)的对象
gameWorld.prototype.removeDeadObjects = function() {
    for (var i = 0; i < this.gameObjects.length; i++) {
        if (this.gameObjects[i].life <= 0) {
            if (this.gameObjects[i].type == 'player') {
                this.state = 'END';
            }
            this.gameObjects.splice(i, 1);
        }
    }
};

// 在游戏环境中把所有对象绘制出来
gameWorld.prototype.drawGame = function(ctx) {
    //清屏
    ctx.clearRect(0, 0, this.w, this.h);

    //画背景
    this.background(ctx, this.w, this.h);

    //把各个对象绘制出来
    for (var i = 0; i < this.gameObjects.length; i++) {
        //画主角的生命值
        if (this.gameObjects[i].type === 'player') {
            ctx.fillStyle = 'rgb(220,220,220)';
            ctx.font = '18px Helvetica, Ariel';
            ctx.fillText(this.gameObjects[i].life, this.w - 10, this.h - 5);
        }
        //当主角生命值>0时画出其它对象们
        if (this.gameObjects[i].life === 0) {
            this.gameObjects[i].draw(ctx, 0);
        } else {
            if (this.gameObjects[i].animate) {
                this.gameObjects[i].tick += 1;
                if (this.gameObjects[i].tick == 8) {
                    this.gameObjects[i].tick = 0;
                    this.gameObjects[i].frame += 1;
                    if (this.gameObjects[i].frame > this.gameObjects[i].maxFrame) {
                        this.gameObjects[i].frame = this.gameObjects[i].minFrame;
                    }
                }
                this.gameObjects[i].draw(ctx, this.gameObjects[i].frame);
            } else {
                this.gameObjects[i].draw(ctx, 1);
            }
        }
    }

    // 绘制得分数据
    ctx.fillStyle = 'rgb(220,220,220)';
    ctx.font = '18px Helvetica, Ariel';
    ctx.fillText(this.score, 10, this.h - 5);
};
	

用JS来绘制"操作按钮显示层"上面的功能按钮

// 暂停按钮触发时的处理:如果原来是在运行的则暂停游戏并改按按钮图案至"继续游戏";如果原来已经在暂停的则继续游戏并改变按钮图案到"暂停"
function togglePause() {
    if (gWorld.state === 'RUNNING') {
        gWorld.state = 'PAUSE';
        pauseBtn.draw = playImg;
    } else if (gWorld.state === 'PAUSE') {
        gWorld.state = 'RUNNING';
        pauseBtn.draw = pauseImg;
        gameLoop();
    }
}

// 绘制双竖线暂停按钮图形
var pauseImg = function(ctx) {
    ctx.fillStyle = 'rgb(255,255,255)';
    ctx.fillRect(this.x, this.y, this.w * 0.33, this.h);
    ctx.fillRect(this.x + (this.w * 0.66), this.y, this.w * 0.33, this.h);
};

// 绘制暂停恢复游戏按钮图形
var playImg = function(ctx) {
    ctx.fillStyle = 'rgb(255,255,255)';
    ctx.beginPath();
    ctx.moveTo(this.x, this.y);
    ctx.lineTo(this.x, this.y + this.h);
    ctx.lineTo(this.x + this.w, this.y + this.h * 0.5);
    ctx.closePath();
    ctx.fill();
};

// 设定"暂停游戏按钮"对象(包括位置座标、宽度、高度以及状态和绘制图形所用的函数)
var pauseBtn = new clickBox({
    x: cvs.width - 40,
    y: 10,
    w: 30,
    h: 30,
    active: false,
    draw: pauseImg
});

// 触碰右边的按钮对象
var touchRightBtn = new clickBox({
    x: cvs.width - 300,
    y: 30,
    w: 300,
    h: cvs.height - 50,
    active: false
});

// 触碰左边的按钮对象
var touchLeftBtn = new clickBox({
    x: 0,
    y: 30,
    w: 300,
    h: cvs.height - 50,
    active: false
});

// 获得鼠标在CANVAS中的相对位置
function getMousePos(canvas, evt) {
    // get canvas position
    var obj = canvas;
    var top = 0;
    var left = 0;
    while (obj && obj.tagName != 'BODY') {
        top += obj.offsetTop;
        left += obj.offsetLeft;
        obj = obj.offsetParent;
    }

    // return relative mouse position
    var mouseX = evt.clientX - left + window.pageXOffset;
    var mouseY = evt.clientY - top + window.pageYOffset;
    return {
        x: mouseX,
        y: mouseY
    }
}

// 测试按钮对象的点击是否有效函数
clickBox.prototype.testClick = function(clickX, clickY) {
    if (this.active) {
        if (clickX >= this.x && clickX <= this.x + this.w) {
            if (clickY >= this.y && clickY <= this.y + this.h) {
                return 1;
            }
        }
    }
    return 0;
}
	

跟游戏环境处理相关的函数

// 初始化游戏环境
function gameWorld(gravity) {
    this.w = 0;
    this.h = 0;
    this.gravity = gravity || 7;
    this.score = 0;
    this.gameObjects = [];
    this.keyPressed = {
        right: false,
        left: false,
        up: false
    };
    this.background = drawForrest;
    this.levelUp = {
        diffInc: 5,
        next: 15,
        allow: false
    };
    this.state = 'MAIN';
}

// 添加对象到游戏环境中去
gameWorld.prototype.addObject = function(newGameObject) {
    this.gameObjects.push(newGameObject);
};

// 添加新对象
gameWorld.prototype.addNewObjects = function() {
    var bananaProb = 50;
    var appleProb = 30 + this.levelUp.diffInc;
    var superBanProb = 200;

    //计算是否生成香蕉对象
    var prob = Math.random() * 1000;
    if (prob < bananaProb) {

        prob = Math.random() * 1000;
        if (prob <= superBanProb) {

            var superBanana = makeBanana(this.w);
            superBanana.w = superBanana.w * 2;
            superBanana.h = superBanana.h * 2;
            superBanana.points = 5;

            this.addObject(superBanana);
        } else {
            this.addObject(makeBanana(this.w));
        }
    }

    //检测是否需要提高难度参数值
    if (this.score >= this.levelUp.next) {
        if (this.levelUp.allow) {
            this.levelUp.diffInc += 7;
            this.levelUp.next += 15;
            this.levelUp.allow = false;
        }
    } else {
        this.levelUp.allow = true;
    }

    //计算是否生成苹果(炸弹)对象
    var prob = Math.random() * 1000;
    if (prob < appleProb) {
        this.addObject(makeApple(this.w));
    }
};

// 设置Y轴下落重力加速度属性的函数
var fall = function(options) {
    this.vY = options.gravity;
};

// 设置对象有solid属性的函数
function isSolid(gObj) {
    var solidObj = gObj;
    solidObj.solid = true;
    return solidObj;
}

// 设置对象有destrutable属性的函数
function isDestructable(gObj) {
    var destObj = gObj;
    destObj.destructable = true;
    return destObj;
}

 // 设置对象有damage属性的函数
function doesDamage(gObj) {
    var damageObj = gObj;
    damageObj.giveDamage = true;
    return damageObj;
}

// 设置对象有givePoint属性的函数
function givesPoints(gObj) {
    var pointsObj = gObj;
    pointsObj.givePoint = true;
    return pointsObj;
}

// 设置主角运动参数的函数
function userControlled(gObj) {
    var userObj = gObj;
    userObj.jump = false;
    userObj.move = function(options) {
        var speed = 5;

        this.vX = 0;
        this.animate = false;
        if (options.keyPressed.right) {
            this.vX += speed;
            this.animate = true;
        }
        if (options.keyPressed.left) {
            this.vX += speed * -1;
            this.animate = true;
        }

        /*gravity and jump testing
        if (this.jump == true) {
        	this.vY = gravity*.5;
        }
        if (keyPressed.up && this.jump == false) {
        	this.vY -= 60;
        	this.jump = true;
        }*/
    };
    return userObj;
}

// 设置对象的动画参数
function isAnimated(gObj, lastFrame, firstFrame) {
    var animatedObj = gObj;
    animatedObj.animate = true;
    animatedObj.frame = 1;
    animatedObj.tick = 0;
    animatedObj.maxFrame = lastFrame;
    animatedObj.minFrame = firstFrame;
    return animatedObj;
}

// 动画的原理就是多桢刷新,下面的这个函数设定每桢的刷新时间
function gameLoop() {
    setTimeout(function() {
        loop = window.requestAnimationFrame(gameLoop);

        gWorld.updateGameObjects();
        gWorld.drawGame(ctx);
        gWorld.removeDeadObjects();
        gWorld.addNewObjects();

        pauseBtn.draw(ctx);

        if (gWorld.state === 'PAUSE' || gWorld.state === 'END') {
            window.cancelAnimationFrame(loop);

            if (gWorld.state === 'END') {
                endScreen();
            }
        }
    }, 1000 / fps);
}

	

游戏初始化

游戏在初始化的时候需要设定CANVAS的尺寸、动画更新时的每秒桢数以及添加对鼠标和键盘等输出设备的相关监听;在完成上述工作后打开游戏封面等待用户按"开始游戏"按钮使游戏状态改变至"运行"状态。

var cvs = document.getElementById('canvas');	// 获得Canvas对象
var ctx = canvas.getContext('2d');	// 获得Canvas的2D对象
cvs.width = 700;		// 设定Canvas对象宽度
cvs.height = 460;	// 设定Canvas对象高度
var loop;
var fps = 60;	// 设定动画每秒桢数

var gWorld = new gameWorld(6);	// 实例化一个游戏环境对象
gWorld.w = cvs.width;	// 设计游戏环境对象的宽度跟Canvas对象宽度一致
gWorld.h = cvs.height;	// 设计游戏环境对象的高度跟Canvas对象高度一致

// 在Canvas中增加”按下鼠标键”的监测
cvs.addEventListener(
	'mousedown',
	function(evt) {
        var mousePos = getMousePos(cvs, evt);
        // 如果是按在"开始游戏按钮"上的,则开始游戏
        if (startBox.testClick(mousePos.x, mousePos.y)) {
            startBox.active = false;
            startGame();
        }
        // 如果是按在"暂停游戏按钮"上的,则暂停游戏
        if (pauseBtn.testClick(mousePos.x, mousePos.y)) {
            togglePause();
        }
        // 如果是按在"重新开始游戏按钮"上的,则重新开始游戏
        if (replayBtn.testClick(mousePos.x, mousePos.y)) {
            startGame();
        }
	}
);

// 在浏览器窗口中增加对键盘按下按钮的监测
window.addEventListener('keydown', function(e) {
    //在游戏封面中按回车直接开始游戏
    if (e.keyCode === 13) {
        if (gWorld.state === 'MAIN' || gWorld.state === 'END') {
            startGame();
        }
    }

    //按P键暂停游戏
    if (e.keyCode === 80) {
        togglePause();
    }

    //光标右键操作主角向右,光标左键操作主角向左
    if (e.keyCode === 39) {
        gWorld.keyPressed.right = true;
    } else if (e.keyCode === 37) {
        gWorld.keyPressed.left = true;
    }

});

// 在浏览器窗口中增加对键盘按键跳上来后的监测
window.addEventListener('keyup', function(e) {
    //通过监测左右光标键是否弹上来(未按状态)来停止向左或向右移动
    if (e.keyCode === 39) {
        gWorld.keyPressed.right = false;
    } else if (e.keyCode === 37) {
        gWorld.keyPressed.left = false;
    }
});

// 显示信息输出
function displayTest(output) {
    //Set the text font and color
    ctx.fillStyle = 'rgb(255,255,255)';
    ctx.font = "18px Helvetica, Ariel";
    ctx.fillText(output, 500, 30);
}

// JQUERY方法-在DOCUMENT加载READY后初始化适配相应浏览器的基于脚本的动画的计时控制(requestAnimationFrame)
$(function() {
        var lastTime = 0;
        var vendors = ['ms', 'moz', 'webkit', 'o'];
        for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
            window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
            window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] 
            || window[vendors[x] + 'CancelRequestAnimationFrame'];
        }

        if (!window.requestAnimationFrame)
            window.requestAnimationFrame = function(callback, element) {
                                        var currTime = new Date().getTime();
                                        var timeToCall = Math.max(0, 16 - (currTime - lastTime));
                                        var id	=	window.setTimeout(
                                                            function() {
                                                                callback(currTime + timeToCall);
                                                            },
                                                            timeToCall
                                                        );
                                        lastTime = currTime + timeToCall;
                                        return id;
                                    };
		if (!window.cancelAnimationFrame)
			window.cancelAnimationFrame	=	function(id) {
                                        clearTimeout(id);
                                    };
	}
);

mainScreen();		//运行绘制游戏封面的函数

	

总结

把上述各个环节整合在一起就成为了一个完整的游戏。游戏编程其实质就是分解各个环节,分别处理,然后再按游戏的剧本流程把这些分解的环节再次整合在一起。这样就能抽丝剥茧、由易入难的完成整个编程。