扯淡前言
今天在 codepen
上发现一个 pong (也就是乒乓)的 Canvas 游戏,做的挺不错的,于是解读了它的源码,从中总结了一种 Canvas 游戏设计的思路和框架。
运用这份框架,我写了一个 贪吃蛇 和 黑桃王 (一种扑克游戏),简单但效果还不错。
所以说框架果然很重要,有了体系和规则,剩下的就是完形填空一般的填写代码的快感。
游戏的设计模式
pong 这个 canvas 游戏由四部分组成:定模型,算模型,改模型,画模型
- 定模型:定义游戏所有数据。如位置,速度,方向,角度,先手,颜色,得分,失分判定等
- 算模型:就是数据更新,重算被修改后的数据。比如方向修改后,要根据方向与速度重算位置,位置修改后,要对可能引起的分数变化修改得分数据。
- 改模型:就是监听器,监听用户动作更改游戏数据
- 画模型:数据到画面的映射
定模型
- 可把所有数据分为两类:常量与变量
- 常量(如拍子的长度和宽度,移动速度,乒乓球的半径等),可以包装成外部对象引入
- 变量又可分为两类:一类是能改变其他数据的数据,另一类是被改变的数据
比如说:方向数据可以通过影响 dx, dy,从而改变位置数据 x, y
算模型
- 专注于用能改变其他数据的数据改变其他数据
改模型
- 专注于改变能改变数据的数据
代码框架
game loop is the heart of the game, it continuously refresh the frame and data model
// 定模型
init() {
...
this.loop()
this.listen()
}
// 算模型 + 画模型
loop() {
this.update()
this.draw()
}
// 改模型
listen() {
}
题外话:游戏其实就是控制我们所看到的画面。由于画面是由数据决定,所以游戏本质上就是控制我们能看的画面所依赖的数据
其他细节
Tips
1 如何播放音效?
new Audio + play
- 第一步 新建 Audio 对象 new Audio(音频文件地址或流字符串)
- 第二步 执行 .play( ) 方法
var snd2 = new Audio("data:audio/mpeg;base64,音频文件的编码");
snd2.play()
2 canvas 清理画布,填充背景,写入文本的代码
- 清理画布的代码
// Clear the Canvas
this.context.clearRect(
0,
0,
this.canvas.width,
this.canvas.height
);
- 填充画布背景的代码
// Set the fill style of back
this.context.fillStyle = this.color;
// Draw the background
this.context.fillRect(
0,
0,
this.canvas.width,
this.canvas.height
);
- 写入文本的代码
// Set the default canvas font and align it to the center
this.context.font = '100px Courier New';
this.context.textAlign = 'center';
// Change the font size for the center score text
this.context.font = '30px Courier New';
// Draw the winning score (center)
this.context.fillText(
'Round ' + (Pong.round + 1),
(this.canvas.width / 2),
35
);
3 按下移动,不按不动怎么做
同时监听按下和松开就行了
- 监听keydown,修改移动方向数据
- 监听keyup,还原移动方向数据
var PRESSING = {
'ArrowUp': false,
'ArrowRight': false,
'ArrowDown': false,
'ArrowLeft': false
}
listen: function() {
document.addEventListener('keydown', e => {
this.direction = DIRECTION[e.code] || DIRECTION.Default
if(PRESSING[e.code] !== undefined) {
PRESSING[e.code] = true
}
})
document.addEventListener('keyup', e => {
if(PRESSING[e.code] !== undefined) {
PRESSING[e.code] = false
}
for(key in PRESSING) {
if(PRESSING[key]) {
return
}
}
this.direction = DIRECTION.Default
})
}
如何添加菜单?
很简单,它只在 init()
中调用,每次 endGame()
通过 init()
间接唤醒。
menu: function() {
//画菜单
}
源码结构
实体模型A + 实体模型B + ... + Game 模型 + Game.init
- Ball 对象
- Paddle 对象
- Game 对象
- 实例化 Game 对象并调用 initialize 方法
Ball {
new: function (incrementedSpeed)
//返回球的基础数据
}
Paddle {
new: function (side)
//side指定拍子在左还是在右,返回拍子的基础数据
}
Game {
initialize: function ()
//定义各项属性,并调用menu(), listen()
endGameMenu: function (text)
//过关或结束画面,text是显示文本
menu : function ()
//绘制开始界面菜单,调用draw()
update: function ()
//更新所有物品的位置参数,包括位置计算,碰撞检测,AI,胜负检测,调endGameMenu()
//通关调_generateRoundColor(),在球接不到时调用_resetTurn()
draw: function ()
//画球,画板子,画网,画信息。画球时调用_turnDelayIsOver(victor, loser)
loop: function ()
//调用update(),draw()
listen: function ()
//监听键盘事件,如果还没开始,调用loop(),否则更改模型数据
_resetTurn: function (victor, loser)
//把球摆好,发球开始新的一回合
_turnDelayIsOver: function ()
//工具:延时函数
_generateRoundColor: function ()
//工具:更新桌布
}
实体模型的分离绑定
第一步
var Paddle = {
new : function(param) {
return {}
}
}
第二步
init: function() {
var player = Paddle.new.call(this, xx) // 用call绑定的目的是使Game对象直接拥有数据
// Paddle.abcd -> Game.abcd
// 这样Game的this可以直接操作abcd
}
loop 方法
在 requestAnimationFrame 中实现 update 与 draw 的循环
loop: function() {
Pong.update() //注意这个函数的三句话全是以实例Pong为对象而不是this
Pong.draw() //Pong = Object.assign({}, Game)
if(!Pong.over) requestAnimationFrame(Pong.loop)
}
update 方法
准备一个this.over变量,true代表游戏结束,false进行。
if(!this.over){ ↔ } //游戏还没结束,常规计算
if (胜利数据判断) {
this.over = true
}
else if (失败数据判断) {
this.over = true
}
if (下一回合数据判断) {} //如果表明进入下一回合,Pong._resetRound()
if (下一球数据判断) {} //如果判断该球接不到(进入下一球),Pong._resetTurn()
draw 方法
清画布,填背景,然后各种 draw
// Clear the Canvas
// Draw the background
// Draw A
// Draw B
// Draw C
// Draw D
// ...
暂无评论