GitLab CI/CDによるDevOpsとゲーム開発
WebGLやWebSocketの進歩により、Adobe Flashのようなプラグインを使用しなくても、ブラウザはゲーム開発プラットフォームとして非常に有効です。 さらに、GitLabとAWSを使用することで、一人のゲーム開発者やゲーム開発チームは、ブラウザベースのゲームをオンラインで簡単にホストすることができます。
このチュートリアルでは、GitLab CI/CDを使った継続的インテグレーション/デプロイメント手法によるゲームのテストとホスティング、DevOpsに焦点を当てます。 GitLab、JavaScript、ゲーム開発の基本に精通していることを前提としています。
ゲーム
私たちのデモゲームは、宇宙を旅するシンプルな宇宙船で構成されており、指定された方向にマウスをクリックして撮影します。
別のゲーム「Dark Nova」の開発開始時に強力なCI/CDパイプラインを作成することは、チームが速いペースで作業するために不可欠でした。 このチュートリアルでは、前回の紹介記事をベースに、以下のステップを説明します:
- 前の記事のコードを使用して、gulpファイルでビルドされた素のPhaserゲームから始めます。
- ユニットテストの追加と実行
-
Bullet
、指定された方向にスポーンするトリガーとなるWeapon
クラスの作成。 - この武器を使用し、画面内を動き回る
Player
クラスの追加。 -
Player
、使用するスプライトを追加します。Weapon
- 継続的インテグレーションと継続的デプロイの手法によるテストとデプロイ
最終的には、master
ブランチにプッシュするたびにテストされ、デプロイされる、プレイ可能なゲームのコアができあがります。また、以下のコンポーネントでブラウザベースのゲームを開始するための定型コードも提供されます:
- TypeScriptと PhaserJsで書かれています。
- Gulpを使ったビルド、実行、テスト
- Chaiと Mochaによるユニットテスト
- GitLabによるCI/CD
- GitLab.com でのコードベースのホスティング
- AWSでの試合開催
- AWSへのデプロイ
要件とセットアップ
私の以前の記事DevOps and Game Devを参照して、基礎となる開発ツール、Hello World のようなゲームの実行、GitLab CI/CD を使ったこのゲームの構築について学んでください。このゲームのリポジトリのmaster
ブランチには、すべての設定が完了したバージョンが含まれています。この記事に沿って作業を進めたい場合は、devops-article
ブランチをクローンして作業することができます:
git clone git@gitlab.com:blitzgren/gitlab-game-demo.git
git checkout devops-article
次に、このWeapon
クラスが通過すると予想される状態のほとんどを例証する、小さなテストのサブセットを作成します。手始めに、lib/tests
というフォルダを作成し、新しいファイルweaponTests.ts
に次のコードを追加します:
import { expect } from 'chai';
import { Weapon, BulletFactory } from '../lib/weapon';
describe('Weapon', () => {
var subject: Weapon;
var shotsFired: number = 0;
// Mocked bullet factory
var bulletFactory: BulletFactory = <BulletFactory>{
generate: function(px, py, vx, vy, rot) {
shotsFired++;
}
};
var parent: any = { x: 0, y: 0 };
beforeEach(() => {
shotsFired = 0;
subject = new Weapon(bulletFactory, parent, 0.25, 1);
});
it('should shoot if not in cooldown', () => {
subject.trigger(true);
subject.update(0.1);
expect(shotsFired).to.equal(1);
});
it('should not shoot during cooldown', () => {
subject.trigger(true);
subject.update(0.1);
subject.update(0.1);
expect(shotsFired).to.equal(1);
});
it('should shoot after cooldown ends', () => {
subject.trigger(true);
subject.update(0.1);
subject.update(0.3); // longer than timeout
expect(shotsFired).to.equal(2);
});
it('should not shoot if not triggered', () => {
subject.update(0.1);
subject.update(0.1);
expect(shotsFired).to.equal(0);
});
});
gulpを使ってこれらのテストをビルドして実行するために、以下のgulp関数も既存のgulpfile.js
:
gulp.task('build-test', function () {
return gulp.src('src/tests/**/*.ts', { read: false })
.pipe(tap(function (file) {
// replace file contents with browserify's bundle stream
file.contents = browserify(file.path, { debug: true })
.plugin(tsify, { project: "./tsconfig.test.json" })
.bundle();
}))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true}) )
.pipe(gulp.dest('built/tests'));
});
gulp.task('run-test', function() {
gulp.src(['./built/tests/**/*.ts']).pipe(mocha());
});
ゲームの最初の部分の実装を開始し、これらのWeapon
テストに合格するようにします。この Weapon
クラスは、指定された方向と速度で弾丸の生成をトリガーするメソッドを公開します。後で、武器をトリガーするためのユーザー入力を結びつけるPlayer
クラスを実装します。src/lib
フォルダに、weapon.ts
ファイルを作成します。Weapon
とBulletFactory
の 2 つのクラスを追加し、Phaser のスプライトとグループオブジェクト、およびゲーム固有のロジックをカプセル化します。
export class Weapon {
private isTriggered: boolean = false;
private currentTimer: number = 0;
constructor(private bulletFactory: BulletFactory, private parent: Phaser.Sprite, private cooldown: number, private bulletSpeed: number) {
}
public trigger(on: boolean): void {
this.isTriggered = on;
}
public update(delta: number): void {
this.currentTimer -= delta;
if (this.isTriggered && this.currentTimer <= 0) {
this.shoot();
}
}
private shoot(): void {
// Reset timer
this.currentTimer = this.cooldown;
// Get velocity direction from player rotation
var parentRotation = this.parent.rotation + Math.PI / 2;
var velx = Math.cos(parentRotation);
var vely = Math.sin(parentRotation);
// Apply a small forward offset so bullet shoots from head of ship instead of the middle
var posx = this.parent.x - velx * 10
var posy = this.parent.y - vely * 10;
this.bulletFactory.generate(posx, posy, -velx * this.bulletSpeed, -vely * this.bulletSpeed, this.parent.rotation);
}
}
export class BulletFactory {
constructor(private bullets: Phaser.Group, private poolSize: number) {
// Set all the defaults for this BulletFactory's bullet object
this.bullets.enableBody = true;
this.bullets.physicsBodyType = Phaser.Physics.ARCADE;
this.bullets.createMultiple(30, 'bullet');
this.bullets.setAll('anchor.x', 0.5);
this.bullets.setAll('anchor.y', 0.5);
this.bullets.setAll('outOfBoundsKill', true);
this.bullets.setAll('checkWorldBounds', true);
}
public generate(posx: number, posy: number, velx: number, vely: number, rot: number): Phaser.Sprite {
// Pull a bullet from Phaser's Group pool
var bullet = this.bullets.getFirstExists(false);
// Set the few unique properties about this bullet: rotation, position, and velocity
if (bullet) {
bullet.reset(posx, posy);
bullet.rotation = rot;
bullet.body.velocity.x = velx;
bullet.body.velocity.y = vely;
}
return bullet;
}
}
最後に、エントリーポイントであるgame.ts
を作り直し、Player
とWeapon
の両オブジェクトを結びつけ、更新ループに追加します。更新されたgame.ts
ファイルは以下のようになります:
import { Player } from "./player";
import { Weapon, BulletFactory } from "./weapon";
window.onload = function() {
var game = new Phaser.Game(800, 600, Phaser.AUTO, 'gameCanvas', { preload: preload, create: create, update: update });
var player: Player;
var weapon: Weapon;
// Import all assets prior to loading the game
function preload () {
game.load.image('player', 'assets/player.png');
game.load.image('bullet', 'assets/bullet.png');
}
// Create all entities in the game, after Phaser loads
function create () {
// Create and position the player
var playerSprite = game.add.sprite(400, 550, 'player');
playerSprite.anchor.setTo(0.5);
player = new Player(game.input, playerSprite, 150);
var bulletFactory = new BulletFactory(game.add.group(), 30);
weapon = new Weapon(bulletFactory, player.sprite, 0.25, 1000);
player.loadWeapon(weapon);
}
// This function is called once every tick, default is 60fps
function update() {
var deltaSeconds = game.time.elapsedMS / 1000; // convert to seconds
player.update(deltaSeconds);
weapon.update(deltaSeconds);
}
}
gulp serve
を実行すれば、走り回って撮影することができます。 素晴らしい! CIパイプラインを更新して、既存のビルドジョブと一緒にテストを実行するようにしましょう。
継続的インテグレーション
私たちの変更がビルドを壊さず、すべてのテストがパスすることを確認するために、私たちは継続的インテグレーション(CI) を利用して、プッシュするたびにこれらのチェックを自動的に実行します。 この記事を読んで、継続的インテグレーション、継続的デリバリー、継続的デプロイ、そしてこれらの方法が GitLab によってどのように活用されるのかを理解しましょう。前回のチュートリアルで、私たちはすでにプッシュするたびにアプリをビルドするための.gitlab-ci.yml
ファイルをセットアップしています。テスト用の新しい CI ジョブをセットアップする必要があり、GitLab CI/CD は gulp から生成されたアーティファクトを使ってビルドジョブの後に実行します。
CI/CD設定ファイルのドキュメントを読んで、その内容を調べ、あなたのニーズに合わせて調整してください。
GitLab CI/CDでゲームを構築しましょう。
テストも実行されるようにビルドジョブを更新する必要があります。既存のbuild
ジョブのscript
配列の末尾にgulp build-test
を追加します。 これらのコマンドが実行されると、GitLab CI/CD のartifacts
によって指定されたbuilt
フォルダ内のすべてにアクセスする必要があることがわかります。これらの依存関係を完全に再プルする必要がないように、node_modules
もキャッシュします。build
ジョブは以下のようになります:
build:
stage: build
script:
- npm i gulp -g
- npm i
- gulp
- gulp build-test
cache:
policy: push
paths:
- node_modules
artifacts:
paths:
- built
GitLab CI/CDでゲームをテストしましょう
ローカルでテストするために、gulp run-tests
を実行します。build
のジョブのように gulp がグローバルにインストールされている必要があります。node_modules
をキャッシュから引っ張ってくるので、npm i
コマンドはあまり実行する必要がありません。デプロイの準備として、アーティファクトにbuilt
フォルダが必要なことが分かっています。これは前のジョブからデフォルトの動作として引き継がれます。最後に、慣習として、build
ジョブの後に実行する必要があることを GitLab CI/CD にtest
ステージをtest
与えることで知らせますtest
。YAML 構造に従うと、ジョブは test
次のようになります:
test:
stage: test
script:
- npm i gulp -g
- npm i
- gulp run-test
cache:
policy: push
paths:
- node_modules/
artifacts:
paths:
- built/
指定した間隔でシュートを行うWeapon
クラスのユニットテストを追加しました。Player
クラスはWeapon
を実装しており、動き回りながらシュートを行うことができます。また、GitLab の CI/CD パイプラインにテストアーティファクトとテストステージを追加しました。.gitlab-ci.yml
, を使うことで、プッシュするたびにテストを実行できるようになります。 .gitlab-ci.yml
ファイル.gitlab-ci.yml
全体は .gitlab-ci.yml
このようになります:
image: node:10
build:
stage: build
script:
- npm i gulp -g
- npm i
- gulp
- gulp build-test
cache:
policy: push
paths:
- node_modules/
artifacts:
paths:
- built/
test:
stage: test
script:
- npm i gulp -g
- npm i
- gulp run-test
cache:
policy: pull
paths:
- node_modules/
artifacts:
paths:
- built/
CI/CDパイプラインの実行
これで完了です!新しいファイルをすべて追加してコミットし、プッシュします。 この時点でリポジトリがどのようになっているかは、この記事に関連する最終コミットを私のサンプルリポジトリで参照してください。 ビルドとテストの両方のステージを適用することで、GitLab はリポジトリへのプッシュごとにそれらを順次実行します。 すべてがうまくいけば、パイプラインの各ジョブに緑色のチェックマークが付きます:
test
のジョブをクリックしてビルドログを表示させれば、テストがパスしたことを確認できます。一番下までスクロールして、テストがパスしたことを確認してください:
$ gulp run-test
[18:37:24] Using gulpfile /builds/blitzgren/gitlab-game-demo/gulpfile.js
[18:37:24] Starting 'run-test'...
[18:37:24] Finished 'run-test' after 21 ms
Weapon
✓ should shoot if not in cooldown
✓ should not shoot during cooldown
✓ should shoot after cooldown ends
✓ should not shoot if not triggered
4 passing (18ms)
Uploading artifacts...
built/: found 17 matching files
Uploading artifacts to coordinator... ok id=17095874 responseStatus=201 Created token=aaaaaaaa Job succeeded
継続的デプロイ
継続的デプロイメントで完全なパイプラインを完成させるために、AWS S3 を使って無料のウェブホスティングと、ビルドのアーティファクトをデプロイするジョブをセットアップしましょう。 GitLab にもGitLab Pagesという無料の静的サイトホスティングサービスがありますが、Dark Nova は特に他の AWS ツールを使うため、AWS S3
を使う必要があります。S3 と GitLab Pages の両方へのデプロイについて説明し、この記事で説明したよりも GitLab CI/CD の原則についてさらに掘り下げたこの記事を読んでください。
S3バケットのセットアップ
- AWSアカウントにログインし、S3に移動します。
- 上部にある「バケットを作成」リンクをクリックします。
- 任意の名前を入力し、次へをクリックします。
- デフォルトのプロパティのまま、次へをクリックします。
- グループの権限を管理]をクリックし、[Everyone]グループの[読み取り]を許可し、[次へ]をクリックします。
- バケットを作成し、S3のバケットリストから選択します。
- 右側の「プロパティ」をクリックし、「静的ウェブサイトホスティング」カテゴリを有効にします。
- ラジオボタンをUse this bucket to host a websiteの選択に更新します。
index.html
とerror.html
をそれぞれ記入します。
AWSシークレットの設定
AWSアカウントの認証情報を使ってAWSにデプロイできるようにする必要がありますが、ソースコードにシークレットを入れたくないのは確かです。 幸いなことに、GitLabは変数を使ってこの解決策を提供しています。 これはIAM管理のために複雑になる可能性があります。 ベストプラクティスとして、rootのセキュリティ認証情報を使うべきではありません。 適切なIAM認証情報の管理はこの記事の範囲外ですが、AWSはroot認証情報を使うことは推奨されず、彼らのベストプラクティスに反することを思い出させてくれます。 ベストプラクティスに従って、同じ2つの認証情報(Key IDとSecret)になるカスタムIAMユーザーの認証情報を自由に使ってください。AWSのIAMベストプラクティスを完全に理解するのは良いアイデアです。 これらの認証情報をGitLabに追加する必要があります:
- AWSアカウントにログインし、セキュリティ資格情報のページに移動します。
-
Access Keys」セクションをクリックし、「Create New Access Key」をクリックします。 キーを作成し、IDとシークレットを保管してください。
- GitLabプロジェクトに移動し、左サイドバーのSettings >CI/CDをクリックします。
-
変数セクションの展開
-
AWS_KEY_ID
という名前のキーを追加し、ステップ2のキーIDをValueフィールドにコピーします。 -
AWS_KEY_SECRET
という名前のキーを追加し、ステップ2のキーシークレットを値フィールドにコピーします。
GitLab CI/CDでゲームをデプロイしましょう。
ビルド成果物をデプロイするために、Shared RunnerにAWS CLIをインストールする必要があります。 Shared Runnerは、成果物をデプロイするためにAWSアカウントで認証できる必要もあります。慣例では、AWS CLIはAWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
を探します。GitLabのCIは、deploy
ジョブのvariables
の部分を使用して、前のセクションで設定した変数を渡す方法を提供してくれます。最後に、master
へのプッシュ時にデプロイonly
が発生するようにディレクティブを追加します。この方法では、すべてのブランチがCIを通して実行され、masterへのマージ(または直接コミット)のみがパイプラインのdeploy
ジョブをトリガーします。これらをまとめると、次のようになります:
deploy:
stage: deploy
variables:
AWS_ACCESS_KEY_ID: "$AWS_KEY_ID"
AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET"
script:
- apt-get update
- apt-get install -y python3-dev python3-pip
- easy_install3 -U pip
- pip3 install --upgrade awscli
- aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete
only:
- master
最後のスクリプトコマンドのリージョンとS3のURLは、あなたのセットアップに合わせて更新してください。 最終的な設定ファイル.gitlab-ci.yml
は以下のようになります:
image: node:10
build:
stage: build
script:
- npm i gulp -g
- npm i
- gulp
- gulp build-test
cache:
policy: push
paths:
- node_modules/
artifacts:
paths:
- built/
test:
stage: test
script:
- npm i gulp -g
- gulp run-test
cache:
policy: pull
paths:
- node_modules/
artifacts:
paths:
- built/
deploy:
stage: deploy
variables:
AWS_ACCESS_KEY_ID: "$AWS_KEY_ID"
AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET"
script:
- apt-get update
- apt-get install -y python3-dev python3-pip
- easy_install3 -U pip
- pip3 install --upgrade awscli
- aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete
only:
- master
結論
デモリポジトリ内には、TypeScript、Mocha、Gulp、PhaserをGitLab CI/CDとうまく連携させるためのボイラープレートコードもあります。 フリーのオープンソースソフトウェアを組み合わせて使用することで、CI/CDパイプライン、ゲーム基盤、ユニットテストがすべて稼働し、驚くほど少ないコードでマスターへのプッシュごとにデプロイされます。 エラーはGitLabのビルドログで簡単にデバッグでき、コミットが成功すると数分以内にゲーム上で変更を確認できます。
Dark Novaで最初から継続的インテグレーションと継続的デプロイを設定することで、迅速かつ安定した開発が可能になります。 必要であれば、別の環境、または複数の環境で変更を簡単にテストすることができます。 マルチプレイヤーゲームのバランス調整とアップデートは継続的で退屈なものですが、GitLab CI/CDで安定したデプロイを信頼することで、変更を素早くプレイヤーに届けることができます。
その他の設定
ここでは、パイプラインのスピードアップや改善につながるアイデアをご紹介します:
- npm の代わりにYarn
- 依存関係やツール(AWS CLIなど)を事前にロードできるカスタムDockerイメージのセットアップ
- カスタムドメインをゲームのS3静的ウェブサイトに転送します。
- 小規模プロジェクトで不要と判断した場合は、ジョブを組み合わせてください。
- キューを回避し、独自のGitLab CI/CDランナーを設定します。