Devops and Game Dev

Posted March 15, 2017 by Ryan

Intro

There are a handful of tutorials concerning both Phaser and Typescript, but they are somewhat old at this point, usually rely on a full Visual Studio install, and don’t go into actually hosting your game. While developing Dark Nova I found a plethora of easy to use, cross-platform tools facilitating good development practices and fast iteration for our team. I’m hoping this series can be a working introduction to devops with indie game dev.

By the end, we will have a simple game with some networking running in AWS, and employing a full CI\CD pipeline through Gitlab. Multiplayer networking architecture and “netcode” are beyond the scope of this series. If you are looking for specifics surrounding networking, Glenn Fiedler has an excellent site on a multitude of those topics (seriously go read his stuff). The itinerary for this series follows:

  1. (This post) Foundational development tools and running a Hello World-like game. Building this game using Gitlab’s CI runners every commit to master
  2. Add Mocha and Chai for some unit tests and a 2nd Stage to the CI pipeline for testing
  3. Deploying our project to AWS by hosting it on S3 and adding a 3rd CI Stage for deployment
  4. Adding some simple networking and a server. Add Terraform to deploy every commit to master
  5. Setup Terraform and Gitlab CI to deploy a new environment on every feature/* branch. Optimize build times using a custom docker image. Further optimize spinning up our own dedicated gitlab job runner.

Prerequisites (* – this post):

  • *Node installed
  • *An account on GitLab
  • An account on AWS. I will be using only free tier capabilities, but know that if you are already using AWS free tier you run the risk of going over the limit (still fairly cheap ~8$ a month for a t2.micro)
  • Terraform installed
  • A text editor (I’m using vs code). I won’t be using any platform-specific extensions or build tools. I use Powershell (I use conemu), but Windows is not required here – in fact it proved to be more difficult when it came to docker in later steps.

Setup

Here we’ll set up git, Typescript, Phaser, npm and gulp. ** If you don’t care to do setup step by step, you can just pull the setup project as of this commit and skip to CI setup. **

Create folders, initialize git, npm, Typescript, Phaser, and install a handful of gulp tools we’ll use to build our project:

# Setup folders
mkdir gitlab-game-demo
cd gitlab-game-demo
mkdir src
mkdir assets
# Just select defaults for git and npm
git init
npm init -y
# Install libraries and typings
npm i -g typescript typings
npm i -S phaser-ce
typings install github:photonstorm/phaser-ce/typescript/typings.json -GD
tsc --init
npm i -D browser-sync browserify del gulp gulp-typescript gulp-uglify tsify typescript vinyl-source-stream
# Install global typings
typings install github:photonstorm/phaser-ce/typescript/typings.json -GD
# Copy necessary js vendor files
cp -Path node_modules/phaser-ce/build/*.min.js -Destination vendor/phaser

npm options above are for -g(lobal), -S(ave dependency), and -D(evelopment dependency). The distinction is if you want the package installed system-wide, for production runtime, and for development-only packages, respectively.

Create a simple .gitignore in your root project – just use Microsoft’s prebuilt one for typescript: https://github.com/Microsoft/TypeScript/blob/master/.gitignore

We’ll be using the code from Phaser’s own ‘Getting Started’ project. Download the image needed here and put into the assets folder, named “phaser.png”:

You’ll want to update your typescript config for now. I have mine set to the following, which is more lax about “implicit any”, excludes folders we don’t want compiling, and defines dist as our output directory:

{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": false,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"outDir": "built",
"target": "ES5"
},
"exclude": [
"node_modules",
"build"
]
}
view raw tsconfig.json hosted with ❤ by GitHub

Create the following game.js file in the src directory:

window.onload = function() {
var game = new Phaser.Game(800, 600, Phaser.AUTO, 'gameCanvas', { preload: preload, create: create });
function preload () {
game.load.image('logo', 'assets/phaser.png');
}
function create () {
var logo = game.add.sprite(game.world.centerX, game.world.centerY, 'logo');
logo.anchor.setTo(0.5, 0.5);
}
}
view raw game.js hosted with ❤ by GitHub

Create an index.html and place it in the src folder:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My Phaser Game</title>
<link rel='shortcut icon' href='/favicon.ico' type='image/x-icon'/ >
<script src="/vendor/phaser/phaser.min.js"></script>
<script src="/game.js"></script>
</head>
<body>
<div id="gameCanvas"></div>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

I use Gulp pretty heavily. It adds a nice layer above the messiness of cramming npm scripts in the package.json, and more control with an array of helpful libraries at your disposal. Create gulpfile.js in your root folder:

var gulp = require("gulp");
var browserify = require("browserify");
var source = require('vinyl-source-stream');
var tsify = require("tsify");
var browserSync = require('browser-sync').create();
var del = require('del');
var paths = {
pages: ['src/**/*.html'],
assets: ['assets/**/*.png'],
vendor: ['vendor/**/*.js']
};
// for now we are going to have to run gulp clean manually
gulp.task('clean', function(cb) {
return del(['built/**'], function(err) {
cb(err);
});
});
gulp.task('copy-pages', function() {
return gulp.src(paths.pages)
.pipe(gulp.dest("built"));
});
gulp.task("copy-assets", function () {
return gulp.src(paths.assets)
.pipe(gulp.dest("built/assets"));
});
gulp.task("copy-vendor", function () {
return gulp.src(paths.vendor)
.pipe(gulp.dest("built/vendor"));
});
gulp.task("default", ["copy-pages", "copy-assets", "copy-vendor"], function () {
return browserify({
basedir: 'src/lib',
debug: true,
entries: ['./game.ts'],
cache: {},
packageCache: {}
})
.plugin(tsify)
.bundle()
.pipe(source('game.js'))
.pipe(gulp.dest("built"));
});
gulp.task('watch', ['default'], function(done) {
browserSync.reload();
done();
});
gulp.task('serve', ['default'], function () {
// Serve files from the root of this project
browserSync.init({
server: {
baseDir: "built"
},
ghostMode: false,
online: false
});
gulp.watch(["src/**/*.*"], ['watch']);
});
view raw gulpfile.js hosted with ❤ by GitHub

Browserify will help package all of your typescript up into a browser friendly entrypoint that you hand off to browser-sync. Browsersync is invaluable in that, along with gulp-watch, it will allow you to recompile and refresh all browsers pointing to your local testing server on a change.

You should now be able to run gulp serve and see your game pop up in a browser!

phaser-browser

Go ahead and change the width of your game in game.ts, save, and watch the changes happen without manually rebuilding or refreshing.

Continuous Integration

Gitlab provides a feature rich CI platform you can plug into in many ways along with free shared runners to build, test, and deploy your projects. It’s incredibly easy get started using a base set of features: create a .gitlab-ci.yml file in the root of your project, define a few things, and push. Gitlab will, by convention, kick off a pipeline conforming to the file’s directions. For now we will simply tell the runner to install the included dependencies, and run the default gulp task which ensures our project builds on every commit to master. Add this file to the root of your project and name it .gitlab-ci.yml:

image: node:6
build:
type: build
script:
- npm i
- npm i gulp -g
- gulp
only:
- master
view raw .gitlab-ci.yml hosted with ❤ by GitHub

Here is a breakdown:

  • image: node:6 tells the runner this is the docker image we need. This will ensure node and npm are installed and available at the command line. You can build your own docker images for more advanced use cases and provide them here as well.
  • build: Defines a new stage called build
    • type: build: Tell Gitlab this stage is of type build. See the docs for details on how Gitlab uses this convention
    • script: define a script block. This is where we can put in line by line commands or kick off our own shell script
    • npm inpm i gulp -g and gulp: you should recognize these as simply installing your package.json dependencies, installing gulp globally, and running the default gulp task, respectively
  • only: Run this stage only on specific branches
    • master Only build master. Commits to other branches will not kick off this stage

Shared runners tend to get pretty backed up. Adding your own runner for your project (it can be in the cloud or your own computer) is left as an exercise to the reader.

Add the file, commit, and push to Gitlab. Go to your Gitlab project page, navigate to the Pipelines tab, and watch that sucker kick off. You can drill down into the stage the pipeline is currently on (remember ours only has one – build) and watch the runner output in real time. You should now see a nice green check indicating a successful build on that last commit:
gitlab-pipeline-pass

In the next post we will add some unit tests to be tested in a test CI stage, and host the game on AWS S3 in a deploy stage.

Back to devlog