使用Kontra.js和Web Maker制作小行星

第一次制作游戏始终是一项艰巨的任务。 有很多东西要学习和理解。 但请放心,我会帮助您完成第一个游戏。 此外,Js13kGames游戏卡纸即将开始。 那么,有什么更好的时间来学习如何制作游戏!

在本教程中,我们将制作经典的街机游戏《 Asteroids》。 如果您对游戏不熟悉,可以阅读有关小行星的更多信息。

我们将使用Kontra.js游戏库。 这是我为JS13kGames游戏果酱建立的轻量级库。

在第一个游戏中使用库的好处是,它可以为您解决所有辛苦的工作。 不必担心如何设置游戏循环或管理所有精灵,您只需关注游戏本身即可。

对于此游戏,我们将制作小行星的简化版本。 只是小行星,玩家飞船和子弹。 这样可以更轻松地进行操作,您随时可以添加更多内容。

我们将使用Web Maker应用程序,因为它可以方便地设置Kontra.js游戏。 启动它-https://webmaker.app/app/

首先,如果您正在参与Js13kGames组合,请从设置中打开Js13kGames模式以设置正确的gamedev环境🙂

现在,单击屏幕顶部的“ 新建”按钮。 Web Maker为您提供了一些不同的模板,以使您快速启动并运行。 从模板列表中,选择Kontra Game Engine ,以使用Kontra.js项目预填充编辑器。

单击“ 添加库”按钮,然后更改JS文件以使用Kontra.js文件的最新版本(例如https://unpkg.com/kontra@6.2.1/kontra.js)。

接下来,展开编辑器的HTML部分,并使用宽度和高度更新canvas元素,以便我们可以玩更大的游戏。

    

我们还希望背景是黑色,游戏要有白色边框,以便我们知道屏幕边缘在哪里。 在编辑器的CSS部分中添加以下内容:

 身体 { 
背景:黑色;
}
画布{
边框:1px纯白色;
}

最后要做的是删除编辑器JS部分中除第一行以外的所有内容。 该模板为您提供了一些入门代码,因此您可以熟悉使用Kontra.js。 由于我们将制作自己的游戏,因此我们可以继续删除大部分游戏,并使用kontra.init()仅保留库的初始化。

  kontra.init(); 

现在已经完成了所有设置,我们可以开始制作游戏了。 让我们开始制作一个小行星游戏对象。

要在Kontra中创建游戏对象或Sprite,我们使用kontra.Sprite()并将所需的任何信息传递给它。

对于我们的小行星,我们将为其传递x和y位置,小行星的速度( dxdy )以及render()函数来告诉子画面如何绘制小行星。 在这种情况下,小行星将只是一个圆。

 让小行星= kontra.Sprite({ 
x:100,
y:100,
dx:Math.random()* 4-2
dy:Math.random()* 4-2
半径:30,
render(){
this.context.strokeStyle ='白色';
this.context.beginPath(); //开始绘制形状
this.context.arc(this.x,this.y,this.radius,0,Math.PI * 2);
this.context.stroke(); //勾勒出圆圈
}
}); asteroid.render();

要移动精灵,我们需要一个游戏循环来更新和渲染每一帧。 游戏循环的updaterender只是游戏引擎在正确的时间为您调用的简单功能。 我们将创建一个kontra.GameLoop()并将其update()render()函数传递给它,以更新和渲染精灵。

之后,我们将调用loop.start()启动游戏循环。

  let loop = kontra.GameLoop({ 
update(){
asteroid.update();
},
render(){
asteroid.render();
}
}); loop.start();

小行星应开始向右下角移动。 但是,当到达边缘时,它将继续离开屏幕。 相反,我们希望小行星环绕边缘。

为此,我们将修改update()函数以在更新小行星后检查其位置,并查看其是否超出屏幕边缘。 如果是的话,我们将其移到相反的边缘。

我们可以使用kontra.getCanvas()访问画布的widthheight属性来检查游戏的尺寸。

继续并更新游戏循环的update()函数:

  update(){ 
让canvas = kontra.getCanvas();
asteroid.update(); //小行星超出了左边缘
如果(asteroid.x <0){
asteroid.x = canvas.width;
}
//小行星超出了右边缘
否则,如果(asteroid.x> canvas.width){
asteroid.x = 0;
}
//小行星超出了顶端
如果(asteroid.y <0){
asteroid.y = canvas.height;
}
//小行星超出底部边缘
否则,如果(asteroid.y> canvas.height){
asteroid.y = 0;
}
}

我们现在有一个移动的小行星。 但是仅仅一个还不够,我们需要更多的球员来投篮。

由于将有多个小行星,我们应该更新代码以处理移动多个精灵的情况,包括飞船和子弹。 我们可以做到的一种方法是将所有子画面存储在一个数组中,并在其上循环以更新所有子画面。

我们还应该将小行星创建代码转换为一个函数,以便可以多次调用它。 该函数应创建一个小行星,然后将其添加到数组中。

在这一点上,我们还应该给小行星增加一些随机性,以使它们都不会朝同一方向移动。 我们需要方向为负数和正数,这样它们就不会总是向右或向右移动。

-22范围似乎是一个不错的范围,并且不会使小行星移动得太快。 要获得此范围,我们可以将Math.random()乘以4,然后乘以2。

如果我们进行了所有更改,则完整的游戏代码应如下所示:

  kontra.init(); 
let sprites = [];函数createAsteroid(){
让小行星= kontra.Sprite({
x:100,
y:100,
dx:Math.random()* 4-2
dy:Math.random()* 4-2
半径:30,
render(){
this.context.strokeStyle ='白色';
this.context.beginPath(); //开始绘制形状
this.context.arc(this.x,this.y,this.radius,0,Math.PI * 2);
this.context.stroke(); //勾勒出圆圈
}
}); sprites.push(asteroid);
}对于(var i = 0; i <4; i ++){
createAsteroid();
} let loop = kontra.GameLoop({
update(){
让canvas = kontra.getCanvas();

sprites.map(sprite => {
sprite.update(); //精灵不在左边缘
如果(sprite.x <0){
sprite.x = canvas.width;
}
//精灵不在右边缘
否则(sprite.x> canvas.width){
sprite.x = 0;
}
// Sprite不在顶部边缘
如果(sprite.y <0){
sprite.y = canvas.height;
}
//精灵不在底部边缘
否则,如果(sprite.y> canvas.height){
sprite.y = 0;
}
});
},
render(){
sprites.map(sprite => sprite.render());
}
}); loop.start();

是时候添加玩家飞船了。 我们将从绘制一个三角形精灵开始。 就像我们对小行星所做的一样,我们将其位置和render()函数传递给它。

 让船= kontra.Sprite({ 
x:300,
y:300,
width:6,//我们稍后将使用它进行碰撞检测
render(){
this.context.beginPath();
//画一个三角形
this.context.moveTo(this.x-5,this.y + 3);
this.context.lineTo(this.x,this.y-12);
this.context.lineTo(this.x + 5,this.y + 3);

this.context.closePath();
this.context.stroke();
}
}); sprites.push(ship);

接下来,我们将允许它旋转来使船移动。

当玩家按下左箭头键时,船应向左旋转,而当按下右箭头键时船应向右旋转。 为此,我们将update()函数传递给飞船精灵,并使用kontra.keyPressed()检查是否按下了这些键。 但是,在执行此操作之前,我们需要使用kontra.initKeys()初始化键盘。

我们还将修改飞船的render()函数,以根据当前旋转绘制三角形。

处理轮换时,要记住一些棘手的事情:

  • 旋转单位为弧度,而不是度。 我发现弧度很难使用,因此通常我会度度工作并将其转换为弧度。
  • 您必须先调用context.save(),然后再旋转精灵,然后再旋转context.restore()。 否则,整个画布将被旋转,而不仅仅是精灵。
  • 零度不是向上,而是在右边。 这是因为零弧度从右边开始。 这也意味着在零度处,我们需要绘制指向右边而不是向上的三角形。
  • 若要将精灵围绕其位置旋转,我们需要调用context.translate()将画布的原点移动到船的中心。 那将使船旋转到位。
  • 由于画布的原点是船的x和y位置,因此所有绘图坐标都应相对于船的位置。 这意味着我们在绘制三角形时可以删除this.xthis.y

ew! 只是为了旋转船而已。 让我们更新飞船代码以完成我们刚刚介绍的所有事情。

  kontra.initKeys(); //辅助函数将度数转换为弧度 
函数degreeToRadians(degrees){
返回度* Math.PI / 180;
}让船= kontra.Sprite({
x:300,
y:300,
width:6,//我们稍后将使用它进行碰撞检测
旋转:0,// 0度在右边
render(){
this.context.save();

//变换原点并围绕原点旋转
//使用船只旋转
this.context.translate(this.x,this.y);
this.context.rotate(degreesToRadians(this.rotation));

//绘制一个向右的三角形
this.context.beginPath();
this.context.moveTo(-3,-5);
this.context.lineTo(12,0);
this.context.lineTo(-3,5); this.context.closePath();
this.context.stroke();
this.context.restore();
},
update(){
//向左或向右旋转船
如果(kontra.keyPressed('left')){
this.rotation + = -4
}
否则,如果(kontra.keyPressed('right')){
this.rotation + = 4
}
}
});

现在,我们已经完成了使船旋转的所有艰苦工作,现在可以使船运动。

当玩家按下向上箭头键时,飞船应朝其面对的方向前进。

我们可以通过使用Math.cos()和Math.sin()分别获得旋转的x和y单位来做到这一点。 然后,我们可以将每个单元乘以我们希望船加速的速度。

每个Kontra.js子图使用ddxddy作为加速度矢量的x和y值。 为了使船舶根据其加速度进行移动,我们调用ship.advance()。

更新Ships update()函数以使船舶前进。

  update(){ 
//向左或向右旋转船
如果(kontra.keyPressed('left')){
this.rotation + = -4
}
否则,如果(kontra.keyPressed('right')){
this.rotation + = 4
}

//朝着船的方向向前移动船
const cos = Math.cos(degreesToRadians(this.rotation));
const sin = Math.sin(degreesToRadians(this.rotation));

如果(kontra.keyPressed('up')){
this.ddx = cos * 0.05;
this.ddy = sin * 0.05;
} this.advance();
}

现在,当您按向上箭头时,船应该向前移动。 但是,即使您放开,船也会不断加速。 相反,我们希望当玩家释放向上箭头键时船停止加速。

如果未按向上箭头键,则可以将ddxddy设置为零来实现。

但是,只要玩家按下向上箭头键,船就可以连续加速。 我们应该设置最大速度,以使船不会变得无法控制。

为此,我们需要检查速度矢量的大小( dxdy ),看看它是否大于最大速度。 如果是这样,我们会降低速度。

再次,更新ships update()函数。

  update(){ 
//向左或向右旋转船
如果(kontra.keyPressed('left')){
this.rotation + = -4
}
否则,如果(kontra.keyPressed('right')){
this.rotation + = 4
} //将船朝其所面对的方向向前移动
const cos = Math.cos(degreesToRadians(this.rotation));
const sin = Math.sin(degreesToRadians(this.rotation)); 如果(kontra.keyPressed('up')){
this.ddx = cos * 0.05;
this.ddy = sin * 0.05;
}
其他{
this.ddx = this.ddy = 0;
} this.advance(); //设置最大速度
const幅度= Math.sqrt(this.dx * this.dx + this.dy * this.dy);
如果(幅度> 5){
this.dx * = 0.95;
this.dy * = 0.95;
}
}

要编码的最后一个精灵是项目符号。

当玩家按下空格键时,飞船应向玩家面对的方向发射子弹。 但是,我们希望限制玩家可以发射的子弹数量,因此它不是无限的子弹流。

我们可以通过跟踪玩家自上次射击以来经过的时间来做到这一点。 如果经过了足够的时间,我们可以通过将子弹添加到sprites数组中来发射另一个子弹。

Kontra.js保证帧速率为60FPS,因此每帧恰好是1/60秒。 使用此工具,我们可以跟踪传入新变量dt

每个子弹应从船的末端开始,并以比船的移动略快的速度移动。 每个项目符号还应该只在屏幕上保留很短的时间。

为此,我们将在项目符号上设置ttl(生存时间)属性。 这个属性告诉精灵它应该存活多少帧。 然后,我们可以通过检查sprite.isAlive()来使用array.filter()从阵列中清除所有失效的子弹。

最后,我们现在有三种不同类型的精灵。 在更新代码时,我们应该为每个精灵提供一个type属性,以便在遍历它们时可以使用它来标识它们。 继续,给小行星一个type: 'asteroid' ,给船一个type: 'ship' ,给子弹一个type: 'bullet'

船舶代码现在应如下所示:

 让船= kontra.Sprite({ 

//确保为小行星指定类型:“小行星”!
类型:“船”,

x:300,
y:300,
width:6,//我们稍后将使用它进行碰撞检测
旋转:0,// 0度在右边
dt:0,//跟踪已过去了多少时间

render(){
this.context.save(); //变换原点,并围绕原点旋转
//使用船只旋转
this.context.translate(this.x,this.y);
this.context.rotate(degreesToRadians(this.rotation)); //绘制一个向右的三角形
this.context.beginPath(); this.context.moveTo(-3,-5);
this.context.lineTo(12,0);
this.context.lineTo(-3,5); this.context.closePath();
this.context.stroke();
this.context.restore();
},
update(){
//向左或向右旋转船
如果(kontra.keyPressed('left')){
this.rotation + = -4
}
否则,如果(kontra.keyPressed('right')){
this.rotation + = 4
} //将船朝其所面对的方向向前移动
const cos = Math.cos(degreesToRadians(this.rotation));
const sin = Math.sin(degreesToRadians(this.rotation));

如果(kontra.keyPressed('up')){
this.ddx = cos * 0.05;
this.ddy = sin * 0.05;
}
其他{
this.ddx = this.ddy = 0;
} this.advance(); //设置最大速度
const幅度= Math.sqrt(this.dx * this.dx + this.dy * this.dy);
如果(幅度> 5){
this.dx * = 0.95;
this.dy * = 0.95;
} //允许玩家每1/4秒发射不超过1枚子弹
this.dt + = 1/60;
如果(kontra.keyPressed('space')&& this.dt> 0.25){
this.dt = 0; 让bullet = kontra.Sprite({
类型:“子弹”,//在三角形的末端开始在船上发射子弹
x:this.x + cos * 12
y:this.y + sin * 12,//使子弹比飞船快一点
dx:this.dx + cos * 5
dy:this.dy + sin * 5,//仅活50帧
ttl:50,//子弹很小
宽度:2
高度:2
白颜色'
}); sprites.push(bullet);
}
}
});

在游戏循环更新代码结束时,您可以过滤掉死的子弹。

  sprites = sprites.filter(sprite => sprite.isAlive()); 

我们快完成了。 现在我们有了子弹,我们需要检查小行星与其他物体之间的碰撞。

我们可以在循环update()函数中执行此操作。 我们将作弊,以使事情保持简单,并假设一切都是一圈。 然后,我们可以使用简单的圆与圆碰撞检查。

使用先前添加的type属性,我们可以确定哪些精灵是小行星,然后找到所有非小行星精灵并检查它们之间的碰撞。

现在, update()函数应如下所示:

  update(){ 
让canvas = kontra.getCanvas();

sprites.map(sprite => {
sprite.update(); //精灵不在左边缘
如果(sprite.x <0){
sprite.x = canvas.width;
}
//精灵不在右边缘
否则(sprite.x> canvas.width){
sprite.x = 0;
}
// Sprite不在顶部边缘
如果(sprite.y <0){
sprite.y = canvas.height;
}
//精灵不在底部边缘
否则,如果(sprite.y> canvas.height){
sprite.y = 0;
}
}); // 碰撞检测
for(let i = 0; i <sprites.length; i ++){//仅检查与小行星的碰撞
如果(sprites [i] .type ==='asteroid'){
for(let j = 0; j <sprites.length; j ++){//不检查小行星与小行星的碰撞
如果(sprites [j] .type!=='asteroid'){
让小行星= sprites [i];
让sprite = sprites [j]; //圆与圆碰撞检测
令dx = asteroid.x-sprite.x;
令dy = asteroid.y-sprite.y;
如果(Math.sqrt(dx * dx + dy * dy)<小行星半径+ sprite.width){
asteroid.ttl = 0;
sprite.ttl = 0; 打破;
}
}
}
}
} sprites = sprites.filter(sprite => sprite.isAlive());
}

随着碰撞检测的启动和运行,我们可以添加游戏的最后一部分:将小行星拆分为三个较小的小行星。

这需要更新我们的createAsteroid()函数,使其能够处于x和y位置,以便我们可以在上一个小行星所在的位置创建小行星。 我们还希望能够通过它的大小,以便我们可以制造更小的小行星。

最后,我们需要更新对函数的前四个调用,以将新信息传递给该函数。

像这样更新函数并调用函数:

 函数createAsteroid((x,y,radius){ 
让小行星= kontra.Sprite({
类型:“小行星”
x:x,
y:y,
dx:Math.random()* 4-2
dy:Math.random()* 4-2
半径:半径,
render(){
this.context.strokeStyle ='白色'; this.context.beginPath(); //开始绘制形状
this.context.arc(this.x,this.y,this.radius,0,Math.PI * 2);
this.context.stroke(); //勾勒出圆圈
}
}); sprites.push(asteroid);
}对于(var i = 0; i <4; i ++){
createAsteroid(100,100,30);
}

然后在游戏循环update()函数中,当发生碰撞时,我们可以在该位置创建三个较小的小行星。 小行星只能分裂两次,因此我们将检查小行星的半径以查看其大小是否足以分裂。

 如果(Math.sqrt(dx * dx + dy * dy)<小行星半径+ sprite.width){ 
asteroid.ttl = 0;
sprite.ttl = 0; //仅在小行星足够大时才对其进行分割
如果(asteroid.radius> 10){
对于(var x = 0; x <3; x ++){
createAsteroid(asteroid.x,asteroid.y,asteroid.radius / 2.5);
}
} break;
}

恭喜,您已经完成了第一款游戏! 我们现在有一个功能齐全的小行星游戏。

这是完整的JavaScript代码供参考:

  kontra.init(); 
let sprites = [];函数createAsteroid((x,y,radius){
让小行星= kontra.Sprite({
类型:“小行星”
x:x,
y:y,
dx:Math.random()* 4-2
dy:Math.random()* 4-2
半径:半径,
render(){
this.context.strokeStyle ='白色';
this.context.beginPath(); //开始绘制形状
this.context.arc(this.x,this.y,this.radius,0,Math.PI * 2);
this.context.stroke(); //勾勒出圆圈
}
});
sprites.push(asteroid);
}对于(var i = 0; i <4; i ++){
createAsteroid(100,100,30);
} kontra.initKeys(); //帮助程序函数将度数转换为弧度
函数degreeToRadians(degrees){
返回度* Math.PI / 180;
}让船= kontra.Sprite({
类型:“船”,
x:300,
y:300,
width:6,//我们稍后将使用它进行碰撞检测
dt:0,//跟踪已过去了多少时间
render(){
this.context.save(); //变换原点并围绕原点旋转
//使用船只旋转
this.context.translate(this.x,this.y);
this.context.rotate(degreesToRadians(this.rotation)); //绘制一个向右的三角形
this.context.beginPath();
this.context.moveTo(-3,-5);
this.context.lineTo(12,0);
this.context.lineTo(-3,5);
this.context.closePath();
this.context.stroke();
this.context.restore();
},
update(){
//向左或向右旋转船
如果(kontra.keyPressed('left')){
this.rotation + = -4
}
否则,如果(kontra.keyPressed('right')){
this.rotation + = 4
} //将船朝其所面对的方向向前移动
const cos = Math.cos(degreesToRadians(this.rotation));
const sin = Math.sin(degreesToRadians(this.rotation)); 如果(kontra.keyPressed('up')){
this.ddx = cos * 0.05;
this.ddy = sin * 0.05;
}
其他{
this.ddx = this.ddy = 0;
} this.advance(); //设置最大速度
const幅度= Math.sqrt(this.dx * this.dx + this.dy * this.dy);
如果(幅度> 5){
this.dx * = 0.95;
this.dy * = 0.95;
} //允许玩家每1/4秒发射不超过1枚子弹
this.dt + = 1/60;
如果(kontra.keyPressed('space')&& this.dt> 0.25){
this.dt = 0; 让bullet = kontra.Sprite({
类型:“子弹”,//在三角形的末端开始在船上发射子弹
x:this.x + cos * 12
y:this.y + sin * 12,//使子弹比飞船快一点
dx:this.dx + cos * 5
dy:this.dy + sin * 5,//仅活50帧
ttl:50,//子弹很小
宽度:2
高度:2
白颜色'
}); sprites.push(bullet);
}
}
}); sprites.push(ship); let loop = kontra.GameLoop({
update(){
让canvas = kontra.getCanvas(); sprites.map(sprite => {
sprite.update(); //精灵不在左边缘
如果(sprite.x <0){
sprite.x = canvas.width;
}
//精灵不在右边缘
否则(sprite.x> canvas.width){
sprite.x = 0;
}
// Sprite不在顶部边缘
如果(sprite.y <0){
sprite.y = canvas.height;
}
//精灵不在底部边缘
否则,如果(sprite.y> canvas.height){
sprite.y = 0;
}
}); // 碰撞检测
for(let i = 0; i <sprites.length; i ++){//仅检查与小行星的碰撞
如果(sprites [i] .type ==='asteroid'){
for(let j = 0; j <sprites.length; j ++){//不检查小行星与小行星的碰撞
如果(sprites [j] .type!=='asteroid'){
让小行星= sprites [i];
让sprite = sprites [j]; //圆与圆碰撞检测
令dx = asteroid.x-sprite.x;
令dy = asteroid.y-sprite.y;
如果(Math.sqrt(dx * dx + dy * dy)<小行星半径+ sprite.width){
asteroid.ttl = 0;
sprite.ttl = 0; //仅在小行星足够大时才对其进行分割
如果(asteroid.radius> 10){
对于(var x = 0; x <3; x ++){
createAsteroid(asteroid.x,asteroid.y,asteroid.radius / 2.5);
}
} break;
}
}
}
}
} sprites = sprites.filter(sprite => sprite.isAlive());
},
render(){
sprites.map(sprite => sprite.render());
}
}); loop.start();

从这里开始,您可以继续自己做下去,并为游戏添加更多功能。 您可以添加播放器寿命,徘徊的UFO,超空间,播放器得分或重置按钮。 或者,您可以完全做其他事情,然后制作自己的游戏。

提醒一下,Js13kGames jam 2018从8月13日CEST开始,这是创建您的第一个游戏的绝佳机会! 通过您的绝妙创作向@ StevenKLambert,@ js13kgames和@webmakerapp发推文。 期待您的游戏!