使用了Crafty.js的游戏

概述

本文演示了使用crafty js游戏引擎开发游戏的方法。 Crafty是一种基于JavaScript的HTML5游戏引擎。 设计它的目的是使开发2D图形游更加容易。 使用crafty开发一个游戏之前,用户必须了解两个重要的事情。 他们是

1. 实体(Entity)
2. 组件(Component)

实体(Entity)

一个实体就是一个存在于游戏世界里的某个对象,并且它具有某种运动行为。 换句话说,用户在一个Crafty游戏屏幕上(没有背景图像)看到的一切都可能是一个实体。

组件(Component)

一个组件指定了一个数据集或者某种运动行为,它们可以被应用到一个或多个实体上。

使用Crafty构建一个游戏

此游戏是一个低功能的冒险类游戏,其目的是四处奔跑,访问森林中的所有村庄。

先决条件

开始开发游戏之前,用户必须下载最新的Crafty.js文件。 接下来,从此存档中下载需要的所有图像和音频资料。 创建assets录,并将下载下来的"crafty_bng_tut_assets"目录中的所有文件拷贝到"assets"目录中。

初始化游戏

首先创建一个文件game.js. 对 js/game.js文件本身只是定义了一个全局可访问的游戏对象,该对象只有一个函数start,该函数实现初始化和启动Crafty "game"。

 Game = {
 
  // Initialize and start the game
  
  start: function() {
    
    // Start crafty and set background color 
    
    Crafty.init(480, 320);
    Crafty.background('green');
  }
}
 

HTML文件index.html只导入了Crafty游戏的库文件“crafty.js”,其次是game.js(其中包含建立独特Crafty游戏的代码)。 最后,一旦页面完全加载,JavaScript的一便会通知浏览器运行“Game.start”函数。

<html>
<head>
 <script src="lib/crafty.js"></script>
 <script src="src/game.js"></script>
 <script>
    window.addEventListener('load', Game.start);
 </script>
</head>
<body>
</body>
</html>

创建一个简单的森林场景

在创建森林场景之前,用户必须知道如何创建可以放置实体的"grid" 和 "tiles"。 现在,通过在网格的每一个边框上放置一颗树,然后在边框中间的广场上随意放一些灌木来创建一些地形。 看看代码如何创建网格,树木和灌木

Game = {
  // This defines our grid's size and the size of each of its tiles
  map_grid: {
    width:  22,
    height: 33,
    tile: {
      width:  16,
      height: 16
    }
  },
 
  // The total width of the game screen. Since the grid takes up the entire screen this is just the width of a tile times the width of the grid
  
  width: function() {
    return this.map_grid.width * this.map_grid.tile.width;
  },
 
  // The total height of the game screen. Since the grid takes up the entire screen this is just the height of a tile times the height of the grid
  
  height: function() {
    return this.map_grid.height * this.map_grid.tile.height;
  },
 
  // Initialize and start the game
  
  start: function() {
    // Start crafty and set a background color so that we can see it's working
    Crafty.init(Game.width(), Game.height());
    Crafty.background('rgb(249, 223, 125)');
 
    // Place a tree at every edge square on our grid of 16x16 tiles
    
    for (var x = 0; x < Game.map_grid.width; x++) {
      for (var y = 0; y < Game.map_grid.height; y++) {
        var at_edge = x == 0 || x == Game.map_grid.width - 1 || y == 0 || y == Game.map_grid.height - 1;
        if (at_edge) {
          
          // Place a tree entity at the current tile
         
          Crafty.e('2D, Canvas, Color')
            .attr({
              x: x * Game.map_grid.tile.width,
              y: y * Game.map_grid.tile.height,
              w: Game.map_grid.tile.width,
              h: Game.map_grid.tile.height
            })
            .color('rgb(20, 125, 40)');
        } 
        else if (Math.random() < 0.06) {
         
          // Place a bush entity at the current tile
         
          Crafty.e('2D, Canvas, Color')
            .attr({
              x: x * Game.map_grid.tile.width,
              y: y * Game.map_grid.tile.height,
              w: Game.map_grid.tile.width,
              h: Game.map_grid.tile.height
            })
            .color('rgb(20, 185, 40)');
        }
      }
    }
  }
}

上面的代码中,“Crafty.e('2D, Canvas, Color')“创建一个实体,该实体使用三个组件:”2D“,"Canvas"和"Color"。 2D组件允许用户设置实体x和y位置(直角坐标)以及对象的高度和宽度。 Canvas组件允许在Canvas DOM元素上绘制实体,从而现实游戏界面。 Color组件允许用户设置实体的颜色。

通过“Components”重用普通的属性和行为。

在上面的代码中,“Tree”和“Bush”创造好了。 如果用户想在“森林场景”中复制多个树木和灌木,这个比较难。 因此,建立我们自己的组件,便可以重复多样实体。 为此,创建component.js文件,并放置所有已创建的组件。 除了“Tree”和“Bush”,我们创建另一个组件'Actor'。 作为一个组件,“Actor”封装了2D,Canvas和Grid组件的使用方法。 看看代码如何创建可重用的组件

//Component.js

// The Grid component allows an element to be located on a grid of tiles

Crafty.c('Grid', {
  init: function() {
    this.attr({
      w: Game.map_grid.tile.width,
      h: Game.map_grid.tile.height
    })
  },
 
  // Locate this entity at the given position on the grid
  
  at: function(x, y) {
    if (x === undefined && y === undefined) {
      return { x: this.x/Game.map_grid.tile.width, y: this.y/Game.map_grid.tile.height }
    } else {
      this.attr({ x: x * Game.map_grid.tile.width, y: y * Game.map_grid.tile.height });
      return this;
    }
  }
});
 
// An "Actor" is an entity that is drawn in 2D on canvas via our logical coordinate grid

Crafty.c('Actor', {
  init: function() {
    this.requires('2D, Canvas, Grid');
  },
});
 
// A Tree is just an Actor with a certain color
Crafty.c('Tree', {
  init: function() {
    this.requires('Actor, Color')
      .color('rgb(20, 125, 40)');
  },
});
 
// A Bush is just an Actor with a certain color
Crafty.c('Bush', {
  init: function() {
    this.requires('Actor, Color')
      .color('rgb(20, 185, 40)');
  },
});

现在game.js文件更简单了,因为它只是引用刚刚创建的组件,而不是定义每一个内嵌实体的所有属性。 一起来看看简化过的game.js文件

Game = {
// This defines our grid's size and the size of each of its tiles
  
  map_grid: {
    width:  24,
    height: 16,
    tile: {
      width:  16,
      height: 16
    }
  },
 
  // The total width of the game screen. Since our grid takes up the entire screen this is just the width of a tile times the width of the grid
 
  width: function() {
    return this.map_grid.width * this.map_grid.tile.width;
  },
 
  // The total height of the game screen. Since our grid takes up the entire screen this is just the height of a tile times the height of the grid
 
  height: function() {
    return this.map_grid.height * this.map_grid.tile.height;
  },
 
  // Initialize and start our game
  
  start: function() {
    // Start crafty and set a background color so that we can see it's working
    Crafty.init(Game.width(), Game.height());
    Crafty.background('rgb(249, 223, 125)');
 
    // Place a tree at every edge square on our grid of 16x16 tiles
    
    for (var x = 0; x < Game.map_grid.width; x++) {
      for (var y = 0; y < Game.map_grid.height; y++) {
        var at_edge = x == 0 || x == Game.map_grid.width - 1 || y == 0 || y == Game.map_grid.height - 1;
 
        if (at_edge) {
          
          // Place a tree entity at the current tile
          
          Crafty.e('Tree').at(x, y);
        } 
        else if (Math.random() < 0.06) {
          
          // Place a bush entity at the current tile
          
          Crafty.e('Bush').at(x, y);
        }
      }
    }
  }
}

现在屏幕看起来是这样的

添加玩家到画面上

添加一个玩家可以控制的实体。 要做到这一点,定义一个名为PlayerCharacter(简写为PC)的新组件。 PC组件需要Actor,MoveTo和Color组件。 Actor和Color组件已经在上面的代码中使用了,但moveTo是新的。 通过使用“鼠标或触摸事件”,MoveTo可以让玩家在屏幕上移动。 若要为PC实体激活此“moveTo”控制方案,需要添加"MoveTo"到PC实体,然后在该实体上调用moveTo()来设置应该移动的速度。 已下载的crafty.js文件不具有“moveTo”功能。 因此,从这里制代码,并添加到crafty.js文件的末尾。
添加下面的代码到Component.js文件中。

// This is the player-controlled character

Crafty.c('PlayerCharacter', {
  init: function() {
    this.requires('Actor, MoveTo, Color')
      .moveTo(4)
      .color('rgb(20, 75, 40)');
  }
});

现在,添加下面的代码到game.js文件中

Game = {
  // Initialize and start our game
 
start: function() {
    // Start crafty and set a background color so that we can see it's working...
    
    // Player character, placed at 5, 5 on our grid
    
    Crafty.e('PlayerCharacter').at(5, 5);
 
    // Place a tree at every edge square on our grid of 16x16 tiles
    //...
  }
}

现在屏幕看起来是这样的

添加要访问的村庄

现在游戏包含了一个PC实体,它可以在树木和灌木之间来回移动的。 当他移动的时候,我们可以通过赋予玩家一些东西来增加一些互动。 他在屏幕上来回移动,我们可以为其添加可以访问的“villages”。 添加村庄只需要增加三部分到代码中。 首先,定义一个新的组件并命名为Village,给它一个独特的色彩,从而可以在游戏板上看到它们。 二,注册一个回调函数,每当玩家进入一个村庄时,便用来处理“visiting”。 最后,在启动流程中添加一些代码,用来在随机位置生成一些初始的村庄。 下面是代码所需的补充:
Component.js

// This is the player-controlled character

Crafty.c('PlayerCharacter', {
  init: function() {
    this.requires('Actor, MoveTo, Color, Collision')
      .moveTo(4)
      .color('rgb(20, 75, 40)')
      
      // Whenever the PC touches a village, respond to the event
      .onHit('Village', this.visitVillage);
  },
   
  // Respond to this player visiting a village
  
  visitVillage: function(data) {
    villlage = data[0].obj;
    villlage.collect();
  }
});
 
// A village is a tile on the grid that the PC must visit in order to win the game

Crafty.c('Village', {
  init: function() {
    this.requires('Actor, Color')
      .color('rgb(170, 125, 40)');
  },
 
  collect: function() {
    this.destroy();
  }
});

Game.js

Game = {
  // ...
  // Initialize and start our game
  
  start: function() {
    // ...
    // Generate up to five villages on the map in random locations
   
    var max_villages = 5;
    for (var x = 0; x < Game.map_grid.width; x++) {
      for (var y = 0; y < Game.map_grid.height; y++) {
        if (Math.random() < 0.02) {
          Crafty.e('Village').at(x, y);
 
          if (Crafty('Village').length >= max_villages) {
            return;
          }
        }
      }
    }
  }
}

现在屏幕看起来是这样的

创建一个胜利画面

现在为玩家添加一个需要完成的目标以及一个胜利画面,当玩家成功完成此目标后,显示胜利的画面。 结束时,统计玩家访问的村庄个数,如果他访问到了最后一个,则现实一个胜利画面说:"Victory!"。 为了实现这一目标,我们使用了Crafty非常有用的内置概念“scenes”。 Scenes是区分游戏中某部分与其他部分的差异。 对于这个游戏,开始于两个场景:一个是用户一直在玩的"Game" 场景,一个是“Victory”场景,用于告诉玩家他已经赢了。 由于场景是代码组织中如此重要的组成部分,为此建立一个新文件“js/ scenes.js”来定义这些场景。 对于“Loading”屏幕,只需要使用一个新的实体以文本方式显示一条简单的信息即可,该实体使用2D,DOM和Text组件。 通过使用标准的DOM + CSS浏览器技术,这三个内置的组件可以很容易地为游戏绘制文字。 胜利屏幕所做的唯一一件事是告诉用户,他已经完成了目标。 这是代码的补充部分
Scenes.js

Crafty.scene('Game', function() {
 
  // A 2D array to keep track of all occupied tiles
  
  this.occupied = new Array(Game.map_grid.width);
  for (var i = 0; i < Game.map_grid.width; i++) {
    this.occupied[i] = new Array(Game.map_grid.height);
    for (var y = 0; y < Game.map_grid.height; y++) {
      this.occupied[i][y] = false;
    }
  }
 
  // Player character, placed at 5, 5 on our grid
  
  this.player = Crafty.e('PlayerCharacter').at(5, 5);
  this.occupied[this.player.at().x][this.player.at().y] = true;
 
  // Place a tree at every edge square on our grid of 16x16 tiles
  
  for (var x = 0; x < Game.map_grid.width; x++) {
    for (var y = 0; y < Game.map_grid.height; y++) {
      var at_edge = x == 0 || x == Game.map_grid.width - 1 || y == 0 || y == Game.map_grid.height - 1;
 
      if (at_edge) {
        // Place a tree entity at the current tile
        Crafty.e('Tree').at(x, y);
        this.occupied[x][y] = true;
      } 
      else if (Math.random() < 0.06 && !this.occupied[x][y]) {
        
        // Place a bush entity at the current tile
       
        Crafty.e('Bush').at(x, y);
        this.occupied[x][y] = true;
      }
    }
  }
 
  // Generate up to five villages on the map in random locations
 
  var max_villages = 5;
  for (var x = 0; x < Game.map_grid.width; x++) {
    for (var y = 0; y < Game.map_grid.height; y++) {
      if (Math.random() < 0.02) {
        if (Crafty('Village').length < max_villages && !this.occupied[x][y]) {
          Crafty.e('Village').at(x, y);
        }
      }
    }
  }
 
  this.show_victory = this.bind('VillageVisited', function() {
    if (!Crafty('Village').length) {
      Crafty.scene('Victory');
    }
  });
}, function() {
  this.unbind('VillageVisited', this.show_victory);
});
 
Crafty.scene('Victory', function() {
  Crafty.e('2D, DOM, Text')
    .attr({ x: 0, y: 0 })
    .text('Victory!');
}, 

Game.js

Game = {
  // ...
  // Initialize and start our game
 
  start: function() {
    // Start crafty and set a background color so that we can see it's working
    Crafty.init(Game.width(), Game.height());
    Crafty.background('rgb(249, 223, 125)');
 
    // Simply start the "Game" scene to get things going
    
    Crafty.scene('Game');
  }
}

使用“sprites”添加丰富的图形并使之动画。

"sprite"是一个2D图像,表示一个单独的可视的元素,如树木或者灌木。 一个sprite图是一个位映射的图像,它包含多个sprite。 根据一个spite图,Crafty可以很容易地将不同的sprite映射到相应的组件和实体上。 开始,通过Crafty.load加载sprite图图像。 然后调用Crafty.sprite,并传递sprite图中每个sprite中的宽度(以像素为单位),asset的路径,和对象,对于不同的sprite,对象定义什么会被调用,然后在sprite图中,每个sprite之间进行垂直方向和水平方向上填充。 有关如何创建sprites的信息,请参考链接。 添加下面的代码到Scene.js文件中

// Handles the loading of binary assets such as images and audio files
Crafty.scene('Loading', function(){
  
  // Draw some text for the player to see in case the file takes a noticeable amount of time to load
  Crafty.e('2D, DOM, Text')
    .text('Loading...')
    .attr({ x: 0, y: Game.height()/2 - 24, w: Game.width() })
    .css($text_css);
 
  // Load our sprite map image
  
  Crafty.load(['assets/16x16_forest_1.gif'],'assets/village1.jpg', function(){
    
    // Once the image is loaded, define the individual sprites in the image each one (spr_tree, etc.) becomes a component these components' names are prefixed with "spr_" to remind us that they simply cause the entity to be drawn with a certain sprite
    
    Crafty.sprite(16, 'assets/16x16_forest_1.gif', {
      spr_tree:    [0, 0],
      spr_bush:    [1, 0],
      spr_player:  [1, 1]
    });
 
    Crafty.sprite(8,'assets/village1.jpg',{
    spr_Village:[1.2,6.3,3.5,4]
    });
    
    // Now that our sprites are ready to draw, start the game
    
    Crafty.scene('Game');
  })

如下图所示,修改component.js文件,实现添加sprite

// A Tree is just an Actor with a certain sprite

Crafty.c('Tree', {
  init: function() {
    this.requires('Actor, Solid, spr_tree');
  },
});
 
// A Bush is just an Actor with a certain sprite

Crafty.c('Bush', {
  init: function() {
    this.requires('Actor, Solid, spr_bush');
  },
});
 
// This is the player-controlled character

Crafty.c('PlayerCharacter', {
  init: function() {
    this.requires('Actor, MoveTo, Collision, spr_player')
      .moveTo(4)
      .onHit('Village', this.visitVillage)
      
      //These next lines define our four animations each call to .animate specifies the name of the animation the x and y coordinates within the sprite map at which the animation set begins the number of animation frames *in addition to* the first one
      .animate('PlayerMovingUp',    0, 0, 2)
      .animate('PlayerMovingRight', 0, 1, 2)
      .animate('PlayerMovingDown',  0, 2, 2)
      .animate('PlayerMovingLeft',  0, 3, 2);
  
  // Watch for a change of direction and switch animations accordingly
    
    var animation_speed = 8;
    this.bind('NewDirection', function(data) {
      if (data.x > 0) {
        this.animate('PlayerMovingRight', animation_speed, -1);
      } else if (data.x < 0) {
        this.animate('PlayerMovingLeft', animation_speed, -1);
      } else if (data.y > 0) {
        this.animate('PlayerMovingDown', animation_speed, -1);
      } else if (data.y < 0) {
        this.animate('PlayerMovingUp', animation_speed, -1);
      } else {
        this.stop();
      }
    });
  },
 
  },
  // Respond to this player visiting a village
  visitVillage: function(data) {
    villlage = data[0].obj;
    villlage.visit();
  }
});
 
// A village is a tile on the grid that the PC must visit in order to win the game

Crafty.c('Village', {
  init: function() {
    this.requires('Actor, spr_village');
  },
 
  // Process a visitation with this village
  
  visit: function() {
    this.destroy();
    Crafty.trigger('VillageVisited', this);
  }
});

现在,最后的画面是这样的

文件附件: