概要
Go言語で2Dのゲームアプリの作り方を調べたので、簡単なゲームをサンプルとして作りました。
こちらにソースコード一式があります。
作成したもの
緑の玉がプレーヤーで、青いお化けが敵です。
ステージ上にランダムに配置された落とし穴に落ちたり、ふわふわ動くお化けに当たったりしたら、ゲームオーバーです。
ゲームのステージは2種類あり、上のものの他にも、雪のステージもあります。
使用するライブラリについて
engoというライブラリを用いることで、クロスプラットフォームなデスクトップゲームアプリができます。
このライブラリを使用する上で必要となる基本的な概念を、以下で説明します。
ライブラリの基本的な概念
Entityとは
スクリーンに描画をされて、毎フレームごとに移動や当たり判定などの何らかの処理を行いたいものがある場合は、それらをEntity
として宣言をする必要があります。
私が作成したゲームだと、緑のプレーヤー、青いお化けの敵、そして地面や草や木の3種類のエンティティをEntity
として登録しています。
Entity
として登録するには、以下のフィールドを保持する構造体を作ります。
type Sample struct {
ecs.BasicEntity
common.RenderComponent
common.SpaceComponent
}
RenderComponentではEntity
の見た目に関する情報を、SpaceComponentでは位置に関する情報を保持します。
Systemとは
上で説明したEntity
を、System
に登録をすることで、画面上に描画処理をしたり毎フレームごとになんらかの処理を行ったりできるようになります。
System
を宣言するには、以下のフィールドを保持する構造体を作ります。
type SampleSystem struct {
texture *common.Texture
sampleEntity *Sample
world *ecs.World
}
texture
は見た目を定義するものであり、sampleEntityは上で説明したEntity
を保持するものです。
そして、作成した構造体に以下の3つのメソッドを持たせます。
func (*SampleSystem) New(w *ecs.World){}
func (*SampleSystem) Remove(ecs.BasicEntity) {}
func (*SampleSystem) Update(dt float32) {}
**New()
はSystem
が作成された時に、Remove()
は削除された時に、Update()
**は毎フレームに、それぞれ呼び出されるので、必要な処理を中に記述します。
通常**New()
では見た目の設定など初期設定を、Update()
**では移動や当たり判定などの処理を、それぞれ行います。
ゲームの作成
詳細なソースコードはGitHubにありますが、ここでは一部をかいつまんで説明します。
背景の作成
地面と草と雲を描画します。
素材はここからとってきます。
この素材の一部をタイルのように画面に張り付けていきます。まずはEntity
とSystem
の宣言です。
// Entity
type Tile struct {
ecs.BasicEntity
common.RenderComponent
common.SpaceComponent
}
// System
type TileSystem struct {
world *ecs.World
// x軸座標
positionX int
// y軸座標
positionY int
tileEntity []*Tile
texture *common.Texture
}
続いて、New()
関数でこれらを描画をしていきます。
**クリックしてコードを展開**
func (ts *TileSystem) New(w *ecs.World){
rand.Seed(time.Now().UnixNano())
ts.world = w
// 落とし穴作成中の状態を保持(0 => 作成していない、1以上 => 作成中)
tileMakingState := 0
// 雲の作成中の状態を保持 (0の場合:作成していない、奇数の場合:{(x+1)/2}番目の雲の前半を作成中、偶数の場合:{x/2}番目の雲の後半を作成中)
cloudMakingState := 0
// 雲の高さを保持
cloudHeight := 0
// タイルの作成
tmp := rand.Intn(2)
var loadTxt string
// ランダムにステージを選ぶ
if tmp == 0 {
loadTxt = "tilemap/tilesheet_grass.png"
} else {
loadTxt = "tilemap/tilesheet_snow.png"
}
Spritesheet = common.NewSpritesheetWithBorderFromFile(loadTxt, 16, 16, 0, 0)
Tiles := make([]*Tile, 0)
for j := 0; j < 2800; j++ {
// 地表の作成
if (j > 10){
if (tileMakingState > 1 && tileMakingState < 4){
for t:= 0; t < 8; t++ {
FallPoint = append(FallPoint,j * 16 - t)
}
} else if (tileMakingState == 0){
// すでに作成中でない場合、たまに落とし穴を作る
randomNum := rand.Intn(10)
if (randomNum == 0) {
FallStartPoint = append(FallStartPoint,j * 16)
tileMakingState = 1
}
}
}
// 描画するタイルを保持
var selectedTile int
// 描画するタイルを選択
switch tileMakingState {
case 0: selectedTile = 1
case 1: selectedTile = 2
case 2: tileMakingState += 1; continue
case 3: tileMakingState += 1; continue
case 4: selectedTile = 0
}
// タイルEntityの作成
tile := &Tile{BasicEntity: ecs.NewBasic()}
// 位置情報の設定
tile.SpaceComponent.Position = engo.Point{
X: float32(j * 16),
Y: float32(237),
}
// 見た目の設定
tile.RenderComponent.Drawable = Spritesheet.Cell(selectedTile)
tile.RenderComponent.SetZIndex(0)
Tiles = append(Tiles, tile)
if (tileMakingState > 0){
if (tileMakingState == 4){
tileMakingState = 0
continue
}
tileMakingState += 1
}
}
for j := 0; j < 2800; j++ {
// 雲の作成
if (cloudMakingState == 0){
randomNum := rand.Intn(6)
if (randomNum < 7 && randomNum % 2 == 1) {
cloudMakingState = randomNum
}
cloudHeight = rand.Intn(70) + 10
}
if (cloudMakingState != 0){
// 雲Entityの作成
cloudTile := cloudMakingState + 9
cloud := &Tile{BasicEntity: ecs.NewBasic()}
cloud.SpaceComponent.Position = engo.Point{
X: float32(j * 16),
Y: float32(cloudHeight),
}
cloud.RenderComponent.Drawable = Spritesheet.Cell(cloudTile)
cloud.RenderComponent.SetZIndex(0)
Tiles = append(Tiles, cloud)
// 前半を作成中であれば、次は後半を作成する
if (cloudMakingState % 2 == 1){
cloudMakingState += 1
} else {
cloudMakingState = 0
}
}
//草の作成
if (!utils.Contains(FallPoint,j * 16)){
// 落とし穴の上には作らない
var grassTile int
randomNum := rand.Intn(18)
if (randomNum < 6) {
grassTile = 26 + randomNum
grass := &Tile{BasicEntity: ecs.NewBasic()}
grass.SpaceComponent.Position = engo.Point{
X: float32(j * 16),
Y: float32(221),
}
grass.RenderComponent.Drawable = Spritesheet.Cell(grassTile)
grass.RenderComponent.SetZIndex(1)
Tiles = append(Tiles, grass)
}
}
}
// 地面の描画
for i := 0; i < 3; i++ {
tileMakingState = 0
for j := 0; j < 2800; j++ {
if (tileMakingState == 0){
// 落とし穴を作る場合
if (utils.Contains(FallStartPoint,j * 16)){
tileMakingState = 1
}
}
// 描画するタイルを保持
var selectedTile int
// 描画するタイルを選択
switch tileMakingState {
case 0: selectedTile = 17
case 1: selectedTile = 18
case 2: tileMakingState += 1; continue
case 3: tileMakingState += 1; continue
case 4: selectedTile = 16
}
tile := &Tile{BasicEntity: ecs.NewBasic()}
tile.SpaceComponent.Position = engo.Point{
X: float32(j * 16),
Y: float32(285 - i * 16),
}
tile.RenderComponent.Drawable = Spritesheet.Cell(selectedTile)
tile.RenderComponent.SetZIndex(0)
Tiles = append(Tiles, tile)
if (tileMakingState > 0){
if (tileMakingState == 4){
tileMakingState = 0
continue
}
tileMakingState += 1
}
}
}
tileMakingState = 0
for _, system := range ts.world.Systems() {
switch sys := system.(type) {
case *common.RenderSystem:
for _, v := range Tiles {
ts.tileEntity = append(ts.tileEntity, v)
sys.Add(&v.BasicEntity, &v.RenderComponent, &v.SpaceComponent)
}
}
}
}
乱数を発生させて、ランダムで落とし穴や草、雲を作成しています。
敵の作成
敵のお化けを作ります。
お化けの画像はこちらからとってきました。
まずはEntityとSystemを宣言します。
type Enemy struct {
ecs.BasicEntity
common.RenderComponent
common.SpaceComponent
// ジャンプの状態(0 => 着地中, 1 => 1ジャンプ中, 2 => 降下中)
jumpState int
// ジャンプの残り時間
jumpDuration int
// 移動の速度(0 ~ 2, 数値が高いほど早い)
velocity int
// 画面から消えているか
ifDissappearing bool
}
type EnemySystem struct {
world *ecs.World
enemyEntity []*Enemy
texture *common.Texture
}
続いて、New()
関数で描画と配置を行います。
**クリックしてコードを展開**
func (es *EnemySystem) New(w *ecs.World){
es.world = w
Enemies := make([]*Enemy, 0)
// ランダムで配置
for i := 0; i < 44800; i++ {
randomNum := rand.Intn(400)
if (randomNum == 0){
// 敵の作成
enemy := Enemy{BasicEntity: ecs.NewBasic()}
enemy.SpaceComponent = common.SpaceComponent{
Position: engo.Point{X:float32(i),Y:float32(212)},
Width: 30,
Height: 30,
}
// 画像の読み込み
texture, err := common.LoadedSprite("pics/ghost.png")
if err != nil {
fmt.Println("Unable to load texture: " + err.Error())
}
enemy.RenderComponent = common.RenderComponent{
Drawable: texture,
Scale: engo.Point{X:1.1, Y:1.1},
}
enemy.RenderComponent.SetZIndex(1)
es.texture = texture
for _, system := range es.world.Systems() {
switch sys := system.(type) {
case *common.RenderSystem:
sys.Add(&enemy.BasicEntity, &enemy.RenderComponent, &enemy.SpaceComponent)
}
}
enemy.velocity = rand.Intn(3)
Enemies = append(Enemies,&enemy)
}
es.enemyEntity = Enemies
}
}
乱数を発生させて、ステージ上のランダムな位置にお化けを発生させます。
そしてUpdate()
関数で、作成されたお化けを移動させます。
**クリックしてコードを展開**
func (es *EnemySystem) Update(dt float32) {
// カメラとプレーヤーの位置を取得
var cameraPosition float32
var playerPositionX float32
for _, system := range es.world.Systems() {
switch sys := system.(type) {
case *common.CameraSystem:
cameraPosition = sys.X()
case *PlayerSystem:
playerPositionX = sys.playerEntity.SpaceComponent.Position.X
}
}
for _, o := range es.enemyEntity{
// 画面に描画されていないオブジェクトは移動処理をしない
if (o.SpaceComponent.Position.X > cameraPosition - 240 && o.SpaceComponent.Position.X < cameraPosition + 200 && !o.ifDissappearing){
// プレーヤーとの当たり判定
if (o.SpaceComponent.Position.X == playerPositionX) {
for _, system := range es.world.Systems() {
switch sys := system.(type) {
case *PlayerSystem:
sys.playerEntity.damage += 1
}
}
}
o.SpaceComponent.Position.X -= float32(o.velocity + 1)
// ジャンプをしていない場合
if (o.jumpState == 0){
o.jumpState = rand.Intn(2) + 1
jumpTemp := rand.Intn(3)
switch (jumpTemp) {
case 0: o.jumpDuration = 15
case 1: o.jumpDuration = 25
case 2: o.jumpDuration = 35
}
}
// ジャンプ処理
if (o.jumpState == 1){
// ジャンプをし終わっていない場合
if (o.jumpDuration > 0){
o.SpaceComponent.Position.Y -= 3
o.jumpDuration -= 1
} else {
// ジャンプをし終わった場合
o.jumpState = 2
}
} else {
// 降下をし終わっていない場合
if (o.SpaceComponent.Position.Y < 212){
o.SpaceComponent.Position.Y += 3
} else {
// 降下し終わった場合
o.jumpState = 0
}
}
}else if (o.ifDissappearing){
o.SpaceComponent.Position.Y += 3
}
}
}
ランダムな高さのジャンプを繰り返しながら、ランダムな速度で移動をさせます。
上にCameraSystem
と出てきますが、これはゲーム内の視点を動かすために、ライブラリで最初から用意されているSystem
です。
プレーヤーの作成
プレーヤーのEntity
とSystem
を宣言します。
type Player struct {
ecs.BasicEntity
common.RenderComponent
common.SpaceComponent
// ジャンプの時間
jumpDuration int
// カメラの進んだ距離
distance int
// 落ちているかどうか
ifFalling bool
// ダメージ
damage int
}
type PlayerSystem struct {
world *ecs.World
playerEntity *Player
texture *common.Texture
}
New()
関数で描画をします。
**クリックしてコードを展開**
func (ps *PlayerSystem) New(w *ecs.World){
ps.world = w
// プレーヤーの作成
player := Player{BasicEntity: ecs.NewBasic()}
// 初期の配置
positionX := int(engo.WindowWidth() / 2)
positionY := int(engo.WindowHeight() - 88)
player.SpaceComponent = common.SpaceComponent{
Position: engo.Point{X:float32(positionX),Y:float32(positionY)},
Width: 30,
Height: 30,
}
// 画像の読み込み
texture, err := common.LoadedSprite("pics/greenoctocat.png")
if err != nil {
fmt.Println("Unable to load texture: " + err.Error())
}
player.RenderComponent = common.RenderComponent{
Drawable: texture,
Scale: engo.Point{X:0.1, Y:0.1},
}
player.RenderComponent.SetZIndex(1)
ps.playerEntity = &player
ps.texture = texture
for _, system := range ps.world.Systems() {
switch sys := system.(type) {
case *common.RenderSystem:
sys.Add(&player.BasicEntity, &player.RenderComponent, &player.SpaceComponent)
}
}
common.CameraBounds = engo.AABB{
Min: engo.Point{X: 0, Y: 0},
Max: engo.Point{X: 40000, Y: 300},
}
}
Update()
関数で、移動をします。
**クリックしてコードを展開**
func (ps *PlayerSystem) Update(dt float32) {
// ダメージが1であればゲームを終了
if ps.playerEntity.damage > 0 {
whenDied(ps)
}
// 落とし穴
if (ps.playerEntity.jumpDuration == 0 && utils.Contains(FallPoint,int(ps.playerEntity.SpaceComponent.Position.X)) ){
ps.playerEntity.ifFalling = true
ps.playerEntity.SpaceComponent.Position.Y += 5
}
// 穴に落ち切ったらライフを0にする
if ps.playerEntity.SpaceComponent.Position.Y > 300 {
ps.playerEntity.damage += 1
}
if(!ps.playerEntity.ifFalling){
// 右移動
if engo.Input.Button("MoveRight").Down() {
// 画面の真ん中より左に位置していれば、カメラを移動せずプレーヤーを移動する
if (int(ps.playerEntity.SpaceComponent.Position.X) < ps.playerEntity.distance + int(engo.WindowWidth()) / 2){
ps.playerEntity.SpaceComponent.Position.X += 5
} else {
// 画面の右端に達していなければプレーヤーを移動する
if (int(ps.playerEntity.SpaceComponent.Position.X) < ps.playerEntity.distance + int(engo.WindowWidth()) - 10){
ps.playerEntity.SpaceComponent.Position.X += 5
}
// カメラを移動する
engo.Mailbox.Dispatch(common.CameraMessage{
Axis: common.XAxis,
Value: 5,
Incremental: true,
})
ps.playerEntity.distance += 5
}
}
// プレーヤーを左に移動
if engo.Input.Button("MoveLeft").Down() {
if int(ps.playerEntity.SpaceComponent.Position.X) > ps.playerEntity.distance + 10{
ps.playerEntity.SpaceComponent.Position.X -= 5
}
}
// プレーヤーをジャンプ
if engo.Input.Button("Jump").JustPressed() {
if ps.playerEntity.jumpDuration == 0 {
ps.playerEntity.jumpDuration = 1
}
}
if ps.playerEntity.jumpDuration != 0 {
ps.playerEntity.jumpDuration += 1
if ps.playerEntity.jumpDuration < 14 {
ps.playerEntity.SpaceComponent.Position.Y -= 5
} else if ps.playerEntity.jumpDuration < 26 {
ps.playerEntity.SpaceComponent.Position.Y += 5
} else {
ps.playerEntity.jumpDuration = 0
}
}
}
}
移動をするだけでなく、落とし穴に落ちたらゲームオーバーにする、などの処理も行なっています。
ゲームの開始
上で作成したSystemなどを用いて、ゲームを動かします。
ゲームプログラムのメインの部分は、以下のようになります。
package main
import (
"bytes"
"engo.io/engo"
"engo.io/engo/common"
"engo.io/ecs"
"image/color"
"golang.org/x/image/font/gofont/gosmallcaps"
"./systems"
)
type myScene struct {}
func (*myScene) Type() string { return "myGame" }
func (*myScene) Preload() {
// 必要なファイルを事前に読み込んでおく
engo.Files.Load("pics/greenoctocat.png", "pics/ghost.png", "tilemap/tilesheet_grass.png", "tilemap/tilesheet_snow.png")
engo.Files.LoadReaderData("go.ttf", bytes.NewReader(gosmallcaps.TTF))
common.SetBackground(color.RGBA{255, 250, 220, 0})
}
func (*myScene) Setup(u engo.Updater){
engo.Input.RegisterButton("MoveRight", engo.KeyD, engo.KeyArrowRight)
engo.Input.RegisterButton("MoveLeft", engo.KeyA, engo.KeyArrowLeft)
engo.Input.RegisterButton("Jump", engo.KeySpace)
world, _ := u.(*ecs.World)
// Systemの追加
world.AddSystem(&common.RenderSystem{})
world.AddSystem(&systems.TileSystem{})
world.AddSystem(&systems.PlayerSystem{})
world.AddSystem(&systems.EnemySystem{})
}
func (*myScene) Exit() {
engo.Exit()
}
func main(){
opts := engo.RunOptions{
Title:"myGame",
Width:400,
Height:300,
StandardInputs: true,
NotResizable:true,
}
engo.Run(opts,&myScene{})
}