我是一个普通大二软件工程系学生,对游戏充满兴趣,也很想开发一款让自己满意的游戏。刚好有机会报名参加2016青瓷杯H5引擎校园大赛第一赛季,尝试用青瓷引擎做H5游戏,很庆幸,我能坚持下来,一步一步完成自己开发游戏的想法;也很开心,我们的作品《球生之路》得到评委的认可,获得一等奖。
接下来跟大家分享下,《球生之路》的制作攻略。通过这个攻略,可以感受游戏开发思路,体验游戏制作过程,同时还可以通过实例感受青瓷引擎的功能。
《球生之路》视频如下:
《球生之路》制作过程如下:
首先,此游戏分为三个部分,分别是开始场景、游戏场景和结束场景。
开始场景
新建场景,并将此场景保存为begin。
首先,新建一个UIRoot节点,然后挂载一个UIImage节点和Node节点。UIImage节点用来放背景图片(背景图片笔者直接合成为一张,这样比较省资源),另一个Node节点是用来做开始游戏的点击事件的载体。在Script文件夹下新建一个文件夹,用来存放开始场景的代码,在此文件夹下新建脚本BeginGameControl.js,在脚本中写入代码
var BeginGameControl = qc.defineBehaviour(‘qc.engine.BeginGameControl‘, qc.Behaviour, function() {
this.scene = "";
}, {
scene: qc.Serializer.STRING
});
BeginGameControl.prototype.onClick = function() {
var self = this;
var load = function() {
self.game.state.load(self.scene, true, function() {
}, function() {
});
};
self.game.timer.add(1, load);
};
写完代码后,将此脚本挂载到Node节点上,并在Inspector面板中,修改scene的值为“level”, “level”为游戏场景的名称。这样开始场景就算完成了。
游戏场景
新建场景,并将此场景保存为level。
导入插件
由于此游戏是物理游戏,所以需要导入插件,在插件管理中勾选Box2Dweb,保存并刷新页面即可
创建基本节点
新建一个场景,命名为level,然后新建一个UIRoot,将其命名为playScene,然后在此节点下新建
Uimage节点:命名为background,此节点用来放背景图片,将背景图片挂载到此节点,
Transform的Anchor Presets,横向和纵向都设置为stretch,
Node节点,命名为levels,此节点用来放置关卡,Transform的Anchor Presets横向和纵向
分别设置为center,stretch,
Sprite节点,命名为Player,此节点是主角,将主角默认图片挂载到此节点上,Transform的
Anchor Presets横向和纵向分别设置为center,top,
Node节点,命名为control,此节点用来放置控制键,Transform的Anchor Presets横向和
纵向分别设置为stretch,bottom,设置AnchoredY为-200, Height为100,
Node节点,命名为score,此节点用来显示分数,Transform的Anchor Presets横向和纵向
分别设置为center,top,设置AnchoredX为0,AnchoredY为35,Width为430,Height为100,
接下来设置控制键,在control节点下新建两个Node节点,一个用来放左右键的节点,命名为direct,设置Transform的AnchoredX为35,宽度为150,高度为60,。
另一个用来放其他键的节点,命名为operate,设置Transform的Anchor Presets的横向为right,AnchoredX为-35,宽度为150,高度为60。
在direct节点下创建两个按钮,一个为left,Transform的AnchoredX、宽度和高度都设置为60,Scale的Y改为-1,,Rotation的R改-180
另一个为right,Transform的Anchor Presets横向设置为right,宽度和高度都设置为60.
在operate节点下也创建两个按钮,一个叫skill,设置Transform的宽度和高度为60
另一个叫jump。Transform的Anchor Presets的横向设置为right,宽度和高度设为60
将图片都挂载到按钮上。
然后设置分数,在score下创建一个UIImage节点,用来放置图片,命名为score,挂上“分数”图片,设置Transform的Anchor Presets的纵向为center,AnchoredX为-150,宽度为170,高度为100。
再创建一个UIText节点,命名为point,用来显示分数,将Font Size设置为90,勾选Glow下的on复选框,选择一个偏黄的颜色,设置Transform的Anchor Presets的纵向为center,AnchoredX为-30,宽度为170,高度为100。
编写脚本
创建PlayerControl.Js脚本,这个脚本是为了给player服务的,也是游戏里逻辑最多的地方,首先先设置要序列化的节点等
// define a user behaviour
var PlayerControl = qc.defineBehaviour(‘qc.engine.PlayerControl‘, qc.Behaviour, function() {
var self = this;
// 用于判断节点是否被按下
self.leftBtnDown = false;
self.rightBtnDown = false;
self.skillBtnDown = false;
self.jumpBtnDown = false;
// 操作键
self.leftKey = qc.Keyboard.A;
self.rightKey = qc.Keyboard.D;
self.skillKey = qc.Keyboard.J;
self.jumpKey = qc.Keyboard.K;
// 用于判断节点对应的按键是否被按下
self.leftKeyDown = false;
self.rightKeyDown = false;
self.skillKeyDown = false;
self.jumpKeyDown = false;
// 用于判断节点对应的按键或节点是否被按下
self.leftDown = false;
self.rightDown = false;
self.skillDown = false;
self.jumpDown = false;
self.onGround = true; // 是否着陆
self.cooling = false; // 技能是否冷却
self.durationTime = 0; // 技能还有多久完成冷却
self.leftTime = 0; // 还剩多少时间回到正常状态
// 状态
self.NORMAL = 0;
self.IN_BALLOON = 1;
self.state = self.NORMAL;
self.score = 0;
self.best = 0;
self.scale = 1;
}, {
// 用于操作的节点
leftNode: qc.Serializer.NODE,
rightNode: qc.Serializer.NODE,
skillNode: qc.Serializer.NODE,
jumpNode: qc.Serializer.NODE,
// 各自状态的图片
normal: qc.Serializer.TEXTURE,
inBalloon: qc.Serializer.TEXTURE,
balloon: qc.Serializer.PREFAB,
parent: qc.Serializer.NODE,
// 分数
point: qc.Serializer.NODE,
// 横向移动速度
xAxisSpeed: qc.Serializer.NUMBER,
// 纵向移动速度
yAxisSpeed: qc.Serializer.NUMBER,
// 跳跃时的速度
jumpSpeed: qc.Serializer.NUMBER,
// 技能冷却时间(毫秒)
coolingTime: qc.Serializer.NUMBER,
levels: qc.Serializer.NODE,
// 生存时间(毫秒)
lifeTime: qc.Serializer.NUMBER,
scene: qc.Serializer.STRING
});
然后加入初始化的代码,用于添加按钮节点事件,键盘事件,主角碰撞事件,初始化scale。
// 初始化
PlayerControl.prototype.awake = function () {
var self = this;
self.addKeyNodeListener();
self.addKeyCodeListener();
self.initScale();
self.gameObject.anchoredY = self.parent.height - 200;
self.state = self.NORMAL;
self.isDead = false;
// 给主角添加碰撞事件
self.gameObject.body.onContact.add(function() {
var objectA = arguments[0].gameObjectA,
objectB = arguments[0].gameObjectB;
var nameA = objectA.name,
nameB = objectB.name;
if (nameA == "balloon" || nameB == "balloon") {
if (nameA == "balloon") {
self.gameObject.anchoredY = (self.gameObject.anchoredY + objectA.anchoredY) / 2;
objectA.destroy();
} else if (nameB == "balloon") {
self.gameObject.anchoredY = (self.gameObject.anchoredY + objectB.anchoredY) / 2;
objectB.destroy();
}
if (self.state != self.IN_BALLOON){
self.change(); // 变换状态
}
} else if (nameA == "platform" || nameB == "platform" ||
nameA == "mushroom" || nameB == "mushroom") {
if (self.state == self.IN_BALLOON){
self.change();
}
} else if (nameA == "cactus" || nameB == "cactus") {
self.gameover(); // 碰到仙人掌,游戏结束
return ;
}
// 判定落地条件
self.onGround = true;
try {
if ((nameA == self.name &&
objectA.getWorldPosition().y > objectB.getWorldPosition().y) ||
(nameB == self.name &&
objectB.getWorldPosition().y > objectA.getWorldPosition().y)) {
self.onGround = false;
}
if (nameA == "mushroom" || nameB == "mushroom") {
self.onGround = false;
}
} catch (Exception) {
}
});
};
因为初始化时的初始scale方法并没有创建,所以我们需要创建此脚本
// 根据界面宽度修改scale大小
PlayerControl.prototype.initScale = function() {
var self = this;
if (self.game.width >= 800) {
self.scale = 1.736;
}
self.gameObject.scaleX = self.scale;
self.gameObject.scaleY = self.scale;
self.xAxisSpeed *= self.scale;
self.yAxisSpeed *= self.scale;
self.jumpSpeed *= (self.scale + 1) / 2;
};
接下来创建按钮点击事件
// 用于给控制键添加点击事件
PlayerControl.prototype.addKeyNodeListener = function() {
var self = this;
if (self.leftNode) {
this.addListener(self.leftNode.onDown, function() {
self.leftBtnDown = true;
});
this.addListener(self.leftNode.onUp, function() {
self.leftBtnDown = false;
});
}
if (self.rightNode) {
this.addListener(self.rightNode.onDown, function() {
self.rightBtnDown = true;
});
this.addListener(self.rightNode.onUp, function() {
self.rightBtnDown = false;
});
}
if (self.skillNode) {
this.addListener(self.skillNode.onDown, function() {
self.skillBtnDown = true;
});
this.addListener(self.skillNode.onUp, function() {
self.skillBtnDown = false;
});
}
if (self.jumpNode) {
this.addListener(self.jumpNode.onDown, function() {
self.jumpBtnDown = true;
});
this.addListener(self.jumpNode.onUp, function() {
self.jumpBtnDown = false;
});
}
};
然后是键盘监听事件
// 添加键盘监听事件
PlayerControl.prototype.addKeyCodeListener = function() {
var self = this;
var input = self.game.input;
var leftKey = self.leftKey;
var rightKey = self.rightKey;
var skillKey = self.skillKey;
var jumpKey = self.jumpKey;
this.addListener(input.onKeyDown, function(keyCode){
if (keyCode == leftKey) {
self.leftKeyDown = true;
}
if (keyCode == rightKey) {
self.rightKeyDown = true;
}
if (keyCode == skillKey) {
self.skillKeyDown = true;
}
if (keyCode == jumpKey) {
self.jumpKeyDown = true;
}
});
this.addListener(input.onKeyUp, function(keyCode){
if (keyCode == leftKey) {
self.leftKeyDown = false;
}
if (keyCode == rightKey) {
self.rightKeyDown = false;
}
if (keyCode == skillKey) {
self.skillKeyDown = false;
}
if (keyCode == jumpKey) {
self.jumpKeyDown = false;
}
});
};
此代码中的核心部分,更新各种事件,方法在后面创建
// 更新
PlayerControl.prototype.update = function () {
var self = this;
var obj = self.gameObject;
var body = obj.body;
// 死亡判定之一:掉掉屏幕下方
if (obj.anchoredY === null || obj.anchoredY >= self.parent.height - self.gameObject.height / 2) {
self.gameover();
return ;
}
self.operatorDown();
self.xAxisMoveListener();
self.jumpListener();
self.yAxisMoveListener();
self.skillListener();
self.backNormal();
self.offset();
};
判定键盘或按扭节点是否被按下所需要写的方法
// 只要按下键盘或点击按扭节点,就能判定某个键成功按下
PlayerControl.prototype.operatorDown = function() {
var self = this;
if (self.leftBtnDown || self.leftKeyDown) {
self.leftDown = true;
} else {
self.leftDown = false;
}
if (self.rightBtnDown || self.rightKeyDown) {
self.rightDown = true;
} else {
self.rightDown = false;
}
if (self.skillBtnDown || self.skillKeyDown) {
self.skillDown = true;
} else {
self.skillDown = false;
}
if (self.jumpBtnDown || self.jumpKeyDown) {
self.jumpDown = true;
} else {
self.jumpDown = false;
}
};
然后我们需要实现那些按键的功能了,首先是技能键,用来放出泡泡,然后是跳跃键,y轴移动和x轴移动
// 按下技能键后的事件
PlayerControl.prototype.skillListener = function () {
var self = this;
var obj = self.gameObject;
self.durationTime -= self.game.time.deltaTime;
// 按下跳跃键并且冷却时间结束
if (self.skillDown && self.durationTime <= 0) {
var balloon = self.game.add.clone(self.balloon, self.parent); // 生成泡泡
balloon.anchoredX = obj.anchoredX;
balloon.anchoredY = obj.anchoredY - obj.height * self.scale - 10; // 设置坐标
self.durationTime = self.coolingTime; // 重置冷却时间
}
};
// 按下跳跃键后的事件
PlayerControl.prototype.jumpListener = function () {
var self = this;
var obj = self.gameObject;
var body = obj.body;
if (self.jumpDown && self.onGround) {
if (self.state == self.IN_BALLOON) {
self.change();
}
body.linearVelocity = new qc.Point(body.linearVelocity.x, -self.jumpSpeed);
self.onGround = false;
}
};
// y轴移动的判定
PlayerControl.prototype.yAxisMoveListener = function () {
var self = this;
var body = self.gameObject.body;
if (self.state == self.IN_BALLOON) {
body.linearVelocity = new qc.Point(body.linearVelocity.x, -self.yAxisSpeed);
}
};
// x轴移动的的判定
PlayerControl.prototype.xAxisMoveListener = function () {
var self = this;
var body = self.gameObject.body;
if ((self.leftDown || self.rightDown) && !(self.leftDown && self.rightDown)) {
if (self.leftDown) {
body.linearVelocity = new qc.Point(-self.xAxisSpeed, body.linearVelocity.y);
}
if (self.rightDown) {
body.linearVelocity = new qc.Point(self.xAxisSpeed, body.linearVelocity.y);
}
} else {
body.linearVelocity = new qc.Point(0, body.linearVelocity.y);
}
};
然后我们写变换状态和偏移场景与死亡事件
// 回到正常状态
PlayerControl.prototype.backNormal = function () {
var self = this;
self.leftTime -= self.game.time.deltaTime;
if (self.state == self.IN_BALLOON && self.leftTime < 0) {
self.change();
}
};
// 主角向上移动,场景往下偏移
PlayerControl.prototype.offset = function () {
var self = this;
var obj = self.gameObject;
var height = self.parent.height / 2;
if (obj.anchoredY < height) {
var levels = self.levels.children;
for(var i = 0; i < levels.length; i++) {
levels[i].anchoredY += 4;
}
var children = self.parent.children;
for(i = 0; i < children.length; i++) {
if (children[i].name == "balloon") {
children[i].anchoredY += 4;
}
}
obj.anchoredY += 4;
self.score += 2;
if (self.score > self.best) {
self.best = self.score;
}
self.point.text = "" + self.score;
}
};
// 变换状态
PlayerControl.prototype.change = function () {
var self = this;
var obj = self.gameObject;
var body = obj.body;
if (self.state == self.NORMAL) {
self.state = self.IN_BALLOON;
obj.texture = self.inBalloon;
self.leftTime = self.lifeTime;
body.linearVelocity = new qc.Point(0, 0);
} else if (self.state == self.IN_BALLOON) {
self.state = self.NORMAL;
obj.texture = self.normal;
self.onGround = false;
}
};
// 死亡事件
PlayerControl.prototype.gameover = function() {
var self = this;
var load = function() {
self.game.state.load(self.scene, true, function() {
}, function() {
console.log(self.scene + ‘场景加载完毕。‘);
self.game.world.find(‘overScene/panel/score‘).text = "" + self.score;
self.game.world.find(‘overScene/panel/best‘).text = "" + self.best;
});
};
self.game.timer.add(1, load);
};
这样,这个代码就写完了,然后新建BalloonControl.js,这个代码是为了控制泡泡的存活时间和上升速度
// define a user behaviour
var BalloonControl = qc.defineBehaviour(‘qc.engine.BallonControl‘, qc.Behaviour, function() {
}, {
// 气球纵向移动速度
yAxisSpeed: qc.Serializer.NUMBER,
// 气球存活时间
lifeTime: qc.Serializer.NUMBER
});
BalloonControl.prototype.update = function () {
var self = this;
var body = self.gameObject.body;
body.linearVelocity = new qc.Point(body.linearVelocity.x, -self.yAxisSpeed);
self.lifeTime -= self.game.time.deltaTime;
if (self.lifeTime <= 0) {
self.gameObject.destroy();
}
};
接下来,创建LevelControl脚本,此脚本用来增加关卡
// define a user behaviour
var LevelControl = qc.defineBehaviour(‘qc.engine.LevelControl‘, qc.Behaviour, function() {
var self = this;
self.scale = 1;
}, {
initScene: qc.Serializer.PREFAB,
levels: qc.Serializer.PREFABS,
height: qc.Serializer.NUMBER
});
LevelControl.prototype.awake = function() {
var self = this;
self.initScale();
self.addInitScene();
};
// 初始化scale
LevelControl.prototype.initScale = function() {
var self = this;
if (self.game.width >= 800) {
self.scale = 1.736;
}
};
LevelControl.prototype.update = function() {
var self = this;
var obj = self.gameObject;
var children = obj.children;
// 增加初始场景
if (children.length <= 0) {
self.addInitScene();
}
// 删除关卡
if (obj.getChildAt(0).anchoredY >= self.game.height) {
var tmp = obj.removeChildAt(0);
tmp = null;
}
// 增加关卡
if (obj.children.length * self.height < self.game.height + self.height * 2) {
var lastY = obj.getChildAt(obj.children.length - 1).anchoredY;
self.game.add.clone(
self.game.math.getRandom(self.levels),
obj);
obj.getChildAt(obj.children.length - 1).anchoredY = lastY - self.height;
}
};
// 增加出生关卡
LevelControl.prototype.addInitScene = function () {
var self = this;
var initScene = self.initScene;
var obj = self.gameObject;
self.game.add.clone(initScene, self.gameObject);
obj.getChildAt(0).anchoredY = -self.height * self.scale;
};
然后再建一个脚本MushroomControl.js,用来控制蘑菇播放Animator动画
// define a user behaviour
var MushroomControl = qc.defineBehaviour(‘qc.engine.MushroomControl‘, qc.Behaviour, function() {
var self = this;
self.scale = 1;
}, {
force: qc.Serializer.NUMBER
});
MushroomControl.prototype.awake = function(){
var self = this;
if (self.game.width >= 1000) {
self.scale = 1.736;
}
self.force *= self.scale;
self.gameObject.body.onContact.add(function() {
var objectA = arguments[0].gameObjectA,
objectB = arguments[0].gameObjectB;
var nameA = objectA.name,
nameB = objectB.name;
if (nameA == self.name) {
objectB.body.linearVelocity = new qc.Point(objectB.body.linearVelocity.x,
self.force);
} else if (nameB == self.name) {
objectA.body.linearVelocity = new qc.Point(objectA.body.linearVelocity.x,
self.force);
}
try {
self.gameObject.Animator.play();
} catch (Exception) {
}
});
};
再新建一个脚本ScaleControl.js,这是用来改变scale的。
// define a user behaviour
var ScaleControl = qc.defineBehaviour(‘qc.engine.ScaleControl‘, qc.Behaviour, function() {
}, {
});
ScaleControl.prototype.awake = function() {
var self = this;
if (self.game.width > 800) {
self.gameObject.scaleX = 1.736;
self.gameObject.scaleY = 1.736;
}
};
做Animator动画
在levels节点下新建一个UIImage节点,命名为mushroom,挂载上蘑菇图片,设置宽度和高度都为56,然后打开动作编辑器,创建文件,设置目标对象为mushroom,添加属性position和scale,然后在中间添加关键帧,接着设置Position.y为-56,Scale.scaleY为2,保存后挂载到mushroom节点上,这样,我们就制作好了动画
做预制件
将MushroomControl.js代码挂载到mushroom节点上,然后设置弹力Force为-8.6,,再将Box2D挂载到此节点上;
在playScene新建UIImage节点balloon,这个节点用来做主角吐出的泡泡,将泡泡图片挂载在此节点上,设置宽度和高度都为56,再将BalloonControl.js挂载在此节点上,设置Y Axis Speed为2,设置Life Time为2000;
在levels节点下新建一个UIImage节点,命名为platform,挂载平台的图片,设置宽度为110,高度为30;
在levels节点下新建一个UIImage节点,命名为cactus,挂载仙人掌的图片,设置宽度和高度都为56。
将上述提到的需要预制的节点mushroom、ballon、platform、cactus分别挂载上Box2D代码,其中,balloon中Box2D的Type设置为Dynamic并勾选fixedRotation属性,再给balloon挂载一个ScaleControl.js脚本,然后将这四个节点拖入prefab中,然后在Hierarchy窗口中将这些节点删除,这样,我们就完成了初步的预制,接着,我们需要利用这些预制件继续合成预制件。
在levels节点创建一个Node节点,命名为bounce,将预制件mushroom和platform拖入到bounce节点下,调整好位置后将bounce节点拖入prefab中,然后删除bounce。
在levels节点创建一个Node节点,命名为death,将预制件cactus和platform拖入到bounce节点下,调整好位置后将death节点拖入prefab中,然后删除death。
接着创建关卡了,每个关卡都设置Transform的Anchor Presets横向为center,纵向为bottom,高度为550。关卡中的元素都是从预制件里复制过来。
在levels节点下创建Node节点,命名为level0,这个是用来当初始关卡的,所以需要设置一块底板居中,其他任意。拖入到prefab预制后将level0节点删除。
然后在levels节点下创建任意个Node节点,每个Node节点是一个关卡,自定义设置关卡中的元素。
挂载脚本
将LevelControl.js脚本挂载到levels节点上,将level0拖入到Init Scene属性中,然后设置关卡高度Height为550,最后设置Levels的Length值,此值是自定义关卡的数量,设置好后将关卡拖入到Levels之中。
将ScaleControl.js脚本拖入到direct、operate和levels中。
最后,将PlayerControl.js脚本挂载到player上,将left、right、skill、jump按钮分别拖动到Left Node、Right Node、Skill Node、Jump Node,将正常状态的主角图片拖到Normal中,再将在泡泡中的图片拖入到In Balloon中,将Balloon预制件拖入到Balloon中,将playScene,point和levels节点分别拖入Parent、Point和Levels属性中,设置X Axis Speed为1,Y Axis Speed(在泡泡中状态时的向上移动的速度)为1.8,Jump Speed为7,Cooling Time(放泡泡的冷却时间)为2000,Life Time(在泡泡中状态的时间)为2000,Scene(结束场景的名称)为over。
到此,这个游戏的核心场景就已经完成。
结束场景
新建场景,并将此场景保存为over。
新建一个UIRoot节点,命名为overScene,在此节点下新建一个UIImage节点,命名为background,挂载背景图片,设置Transform的Anchor Presets横向和纵向都为stretch,并设置Left、Right、Top、Bottom值为0.。
在overScene节点下再建一个UIImage节点,命名为panel,挂载分数面板图片,设置Transform的Anchor Presets横向为center,纵向为middle,AnchoredX和AnchoredY为0,宽度为600,高度为436,在此节点下再创建三个节点:
Button节点,命名为replay,用于检测重玩是否被按下,设置Transform的Anchor Presets
横向为center,纵向为top,AnchoredX为0,AnchoredY为250,宽度和高度
为100;
UIText节点:命名为score,显示本局所得分数,设置Transform中的AnchoredX为200,
AnchoredY为60,宽度为120,高度为30,然后在UIText中修改Font Size为
50;
UIText节点:命名为best,显示历史最高分数,设置Transform中的AnchoredX为200,
AnchoredY为140,宽度为120,高度为30,然后在UIText中修改Font Size为
50;
在overScene节点下建一个UIImage节点,命名为mask,设置Transform的Anchor Presets
横向和纵向为stretch,Left、Right、Top和Bottom的值为0,设置Alpha为0.2。
然后新建脚本ReplayControl.js脚本
// define a user behaviour
var ReplayControl = qc.defineBehaviour(‘qc.engine.ReplayControl‘, qc.Behaviour, function() {
var self = this;
self.replayKey = qc.Keyboard.SPACEBAR;
}, {
scene: qc.Serializer.STRING,
best: qc.Serializer.NODE
});
ReplayControl.prototype. awake = function() {
var self = this;
var load = function() {
self.game.state.load(self.scene, true, function() {
}, function() {
console.log(self.scene + ‘场景加载完毕。‘);
var playerScript = self.game.world.find(‘playScene/player‘).getScript("qc.engine.PlayerControl");
playerScript.best = parseInt(self.best.text);
});
};
self.game.timer.add(1, load);
};
将此代码挂载到replay节点上,并将Scene设置为level,将best节点拖入到Best属性中