DevOps and Game Dev with GitLab CI/CD

原文:https://docs.gitlab.com/ee/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/

DevOps and Game Dev with GitLab CI/CD

随着 WebGL 和 WebSockets 的进步,浏览器作为游戏开发平台非常可行,而无需使用 Adobe Flash 之类的插件. 此外,通过使用 GitLab 和AWS ,单个游戏开发人员以及游戏开发团队可以轻松地在线托管基于浏览器的游戏.

在本教程中,我们将专注于 DevOps,以及使用GitLab CI / CD使用持续集成/部署方法测试和托管游戏. 我们假设您熟悉 GitLab,JavaScript 和游戏开发的基础知识.

The game

我们的演示游戏由一个简单的太空飞船组成,该太空飞船通过在给定方向上单击鼠标进行射击.

在开发另一款游戏Dark Nova的开始时,创建强大的 CI / CD 流水线对于团队工作的快速节奏至关重要. 本教程将以我之前的介绍性文章为基础,并执行以下步骤:

  1. 使用上一篇文章中的代码开始由 gulp 文件构建的准相位游戏
  2. 添加和运行单元测试
  3. 创建可以触发以给定方向生成BulletWeapon
  4. 添加使用此武器并在屏幕上四处移动的Player
  5. 添加我们将用于PlayerWeapon的精灵
  6. 使用持续集成和持续部署方法进行测试和部署

到最后,我们将拥有一个可玩游戏的核心,该游戏在每次推送到代码库 master分支时都经过测试和部署. 这还将提供样板代码,用于启动具有以下组件的基于浏览器的游戏:

Requirements and setup

请参考我以前的文章DevOps 和 Game Dev,以学习基础开发工具,运行类似于 Hello World 的游戏,以及从每次新推手到使用 GitLab CI / CD 来构建此游戏. 此游戏存储库master分支包含具有所有配置的完整版本. 如果您想继续阅读本文,可以从devops-article分支进行克隆并进行工作:

  1. git clone git@gitlab.com:blitzgren/gitlab-game-demo.git
  2. git checkout devops-article

接下来,我们将创建一小部分测试,以举例说明我希望该Weapon类经历的大多数状态. 首先,创建一个名为lib/tests的文件夹,并将以下代码添加到一个新文件weaponTests.ts

  1. import { expect } from 'chai';
  2. import { Weapon, BulletFactory } from '../lib/weapon';
  3. describe('Weapon', () => {
  4. var subject: Weapon;
  5. var shotsFired: number = 0;
  6. // Mocked bullet factory
  7. var bulletFactory: BulletFactory = <BulletFactory>{
  8. generate: function(px, py, vx, vy, rot) {
  9. shotsFired++;
  10. }
  11. };
  12. var parent: any = { x: 0, y: 0 };
  13. beforeEach(() => {
  14. shotsFired = 0;
  15. subject = new Weapon(bulletFactory, parent, 0.25, 1);
  16. });
  17. it('should shoot if not in cooldown', () => {
  18. subject.trigger(true);
  19. subject.update(0.1);
  20. expect(shotsFired).to.equal(1);
  21. });
  22. it('should not shoot during cooldown', () => {
  23. subject.trigger(true);
  24. subject.update(0.1);
  25. subject.update(0.1);
  26. expect(shotsFired).to.equal(1);
  27. });
  28. it('should shoot after cooldown ends', () => {
  29. subject.trigger(true);
  30. subject.update(0.1);
  31. subject.update(0.3); // longer than timeout
  32. expect(shotsFired).to.equal(2);
  33. });
  34. it('should not shoot if not triggered', () => {
  35. subject.update(0.1);
  36. subject.update(0.1);
  37. expect(shotsFired).to.equal(0);
  38. });
  39. });

为了使用gulpfile.js构建和运行这些测试,我们还要将以下gulpfile.js函数添加到现有的gulpfile.js文件中:

  1. gulp.task('build-test', function () {
  2. return gulp.src('src/tests/**/*.ts', { read: false })
  3. .pipe(tap(function (file) {
  4. // replace file contents with browserify's bundle stream
  5. file.contents = browserify(file.path, { debug: true })
  6. .plugin(tsify, { project: "./tsconfig.test.json" })
  7. .bundle();
  8. }))
  9. .pipe(buffer())
  10. .pipe(sourcemaps.init({loadMaps: true}) )
  11. .pipe(gulp.dest('built/tests'));
  12. });
  13. gulp.task('run-test', function() {
  14. gulp.src(['./built/tests/**/*.ts']).pipe(mocha());
  15. });

我们将开始实施游戏的第一部分,并通过这些Weapon测试. Weapon类将公开一种方法,以给定的方向和速度触发子弹的生成. 稍后,我们将实现一个Player类,将用户输入绑定在一起以触发武器. 在src/lib文件夹中,创建一个weapon.ts文件. 我们将添加两个类: WeaponBulletFactory ,它们将封装 Phaser 的spritegroup对象,以及游戏特定的逻辑.

  1. export class Weapon {
  2. private isTriggered: boolean = false;
  3. private currentTimer: number = 0;
  4. constructor(private bulletFactory: BulletFactory, private parent: Phaser.Sprite, private cooldown: number, private bulletSpeed: number) {
  5. }
  6. public trigger(on: boolean): void {
  7. this.isTriggered = on;
  8. }
  9. public update(delta: number): void {
  10. this.currentTimer -= delta;
  11. if (this.isTriggered && this.currentTimer <= 0) {
  12. this.shoot();
  13. }
  14. }
  15. private shoot(): void {
  16. // Reset timer
  17. this.currentTimer = this.cooldown;
  18. // Get velocity direction from player rotation
  19. var parentRotation = this.parent.rotation + Math.PI / 2;
  20. var velx = Math.cos(parentRotation);
  21. var vely = Math.sin(parentRotation);
  22. // Apply a small forward offset so bullet shoots from head of ship instead of the middle
  23. var posx = this.parent.x - velx * 10
  24. var posy = this.parent.y - vely * 10;
  25. this.bulletFactory.generate(posx, posy, -velx * this.bulletSpeed, -vely * this.bulletSpeed, this.parent.rotation);
  26. }
  27. }
  28. export class BulletFactory {
  29. constructor(private bullets: Phaser.Group, private poolSize: number) {
  30. // Set all the defaults for this BulletFactory's bullet object
  31. this.bullets.enableBody = true;
  32. this.bullets.physicsBodyType = Phaser.Physics.ARCADE;
  33. this.bullets.createMultiple(30, 'bullet');
  34. this.bullets.setAll('anchor.x', 0.5);
  35. this.bullets.setAll('anchor.y', 0.5);
  36. this.bullets.setAll('outOfBoundsKill', true);
  37. this.bullets.setAll('checkWorldBounds', true);
  38. }
  39. public generate(posx: number, posy: number, velx: number, vely: number, rot: number): Phaser.Sprite {
  40. // Pull a bullet from Phaser's Group pool
  41. var bullet = this.bullets.getFirstExists(false);
  42. // Set the few unique properties about this bullet: rotation, position, and velocity
  43. if (bullet) {
  44. bullet.reset(posx, posy);
  45. bullet.rotation = rot;
  46. bullet.body.velocity.x = velx;
  47. bullet.body.velocity.y = vely;
  48. }
  49. return bullet;
  50. }
  51. }

最后,我们将重做我们的入口点game.ts ,将PlayerWeapon对象绑定在一起,并将它们添加到更新循环中. 这是更新的game.ts文件的外观:

  1. import { Player } from "./player";
  2. import { Weapon, BulletFactory } from "./weapon";
  3. window.onload = function() {
  4. var game = new Phaser.Game(800, 600, Phaser.AUTO, 'gameCanvas', { preload: preload, create: create, update: update });
  5. var player: Player;
  6. var weapon: Weapon;
  7. // Import all assets prior to loading the game
  8. function preload () {
  9. game.load.image('player', 'assets/player.png');
  10. game.load.image('bullet', 'assets/bullet.png');
  11. }
  12. // Create all entities in the game, after Phaser loads
  13. function create () {
  14. // Create and position the player
  15. var playerSprite = game.add.sprite(400, 550, 'player');
  16. playerSprite.anchor.setTo(0.5);
  17. player = new Player(game.input, playerSprite, 150);
  18. var bulletFactory = new BulletFactory(game.add.group(), 30);
  19. weapon = new Weapon(bulletFactory, player.sprite, 0.25, 1000);
  20. player.loadWeapon(weapon);
  21. }
  22. // This function is called once every tick, default is 60fps
  23. function update() {
  24. var deltaSeconds = game.time.elapsedMS / 1000; // convert to seconds
  25. player.update(deltaSeconds);
  26. weapon.update(deltaSeconds);
  27. }
  28. }

运行gulp serve ,您可以四处奔跑射击. 精彩! 让我们更新 CI 管道,使其包括运行测试以及现有的构建作业.

Continuous Integration

为了确保我们的更改不会破坏构建并且所有测试仍然通过,我们利用持续集成(CI)来为每次推送自动运行这些检查. 通读本文以了解持续集成,持续交付和持续部署 ,以及 GitLab 如何利用这些方法. 在上一教程中,我们已经设置了一个.gitlab-ci.yml文件,用于从每次推送开始构建我们的应用程序. 我们需要设置一个新的 CI 作业进行测试,GitLab CI / CD 将使用我们从 gulp 生成的工件在构建作业后运行.

请通读CI / CD 配置文件文档,以探索其内容并根据需要进行调整.

Build your game with GitLab CI/CD

我们需要更新构建作业以确保测试也能运行. 将gulp build-test添加到现有build作业的script数组的末尾. 一旦这些命令运行,我们就知道需要访问 GitLab CI / CD artifacts提供的built文件夹中的所有内容. 我们还将缓存node_modules以避免不得不完全重新拉动那些依赖项:只需将它们打包在缓存中即可. 这是完整的build工作:

  1. build:
  2. stage: build
  3. script:
  4. - npm i gulp -g
  5. - npm i
  6. - gulp
  7. - gulp build-test
  8. cache:
  9. policy: push
  10. paths:
  11. - node_modules
  12. artifacts:
  13. paths:
  14. - built

Test your game with GitLab CI/CD

对于本地测试,我们只需要运行gulp run-tests ,这需要像build作业一样在全球范围内安装 gulp. 我们从缓存中拉出node_modules ,因此npm i命令不必做太多事情. 在准备部署时,我们知道我们仍然需要工件中的built文件夹,该文件夹将作为上一个作业的默认行为被带入. 最后,按照惯例,我们通过给 GitLab CI / CD 一个test 阶段来告知需要在build工作之后运行它. 按照 YAML 结构, test作业应如下所示:

  1. test:
  2. stage: test
  3. script:
  4. - npm i gulp -g
  5. - npm i
  6. - gulp run-test
  7. cache:
  8. policy: push
  9. paths:
  10. - node_modules/
  11. artifacts:
  12. paths:
  13. - built/

我们为以指定间隔射击的Weapon类添加了单元测试. Player类实现了Weapon以及四处移动和射击的能力. 此外,我们还使用.gitlab-ci.yml在 GitLab CI / CD 管道中添加了测试工件和测试阶段,从而使我们能够在每次推送时运行测试. 现在,我们整个.gitlab-ci.yml文件应如下所示:

  1. image: node:10
  2. build:
  3. stage: build
  4. script:
  5. - npm i gulp -g
  6. - npm i
  7. - gulp
  8. - gulp build-test
  9. cache:
  10. policy: push
  11. paths:
  12. - node_modules/
  13. artifacts:
  14. paths:
  15. - built/
  16. test:
  17. stage: test
  18. script:
  19. - npm i gulp -g
  20. - npm i
  21. - gulp run-test
  22. cache:
  23. policy: pull
  24. paths:
  25. - node_modules/
  26. artifacts:
  27. paths:
  28. - built/

Run your CI/CD pipeline

而已! 添加所有新文件,提交并推送. 有关此时存储库外观的参考,请参考示例存储库中与本文相关最终提交 . 通过同时应用构建和测试阶段,GitLab 将在每次推送到我们的存储库时按顺序运行它们. 如果一切顺利,您将在管道的每个作业上得到一个绿色的复选标记:

Passing Pipeline

您可以通过单击test作业以输入完整的构建日志来确认测试通过. 滚动到底部,观察所有过去的荣耀:

  1. $ gulp run-test
  2. [18:37:24] Using gulpfile /builds/blitzgren/gitlab-game-demo/gulpfile.js
  3. [18:37:24] Starting 'run-test'...
  4. [18:37:24] Finished 'run-test' after 21 ms
  5. Weapon
  6. should shoot if not in cooldown
  7. should not shoot during cooldown
  8. should shoot after cooldown ends
  9. should not shoot if not triggered
  10. 4 passing (18ms)
  11. Uploading artifacts...
  12. built/: found 17 matching files
  13. Uploading artifacts to coordinator... ok id=17095874 responseStatus=201 Created token=aaaaaaaa Job succeeded

Continuous Deployment

我们在每次推送时都构建并测试了代码库. 为了完成持续部署的全部流程,让我们使用 AWS S3设置免费的 Web 托管,并通过一项工作来部署构建工件. GitLab 还提供了免费的静态站点托管服务GitLab Pages ,但是 Dark Nova 特别使用需要使用AWS S3其他 AWS 工具. 通读本文,该文章描述了如何同时部署到 S3 和 GitLab 页面,并且比本文讨论的内容更深入地研究 GitLab CI / CD 的原理.

Set up S3 Bucket

  1. 登录您的 AWS 账户并转到S3
  2. 点击顶部的创建存储桶链接
  3. 输入您选择的名称,然后单击下一步
  4. 保留默认属性 ,然后单击下一步
  5. 单击” 管理组”权限 ,然后为” 所有人”组允许” 读取 “,单击”下一步”.
  6. 创建存储桶,然后在您的 S3 存储桶列表中选择它
  7. 在右侧,单击” 属性”并启用” 静态网站托管”类别
  8. Update the radio button to the 使用此存储桶托管网站 selection. Fill in index.html and error.html respectively

Set up AWS Secrets

我们需要能够使用我们的 AWS 账户凭证部署到 AWS,但是我们当然不希望在源代码中添加机密信息. 幸运的是,GitLab 通过Variables提供了解决方案. 由于IAM管理,这可能会变得复杂. 作为最佳实践,您不应使用根安全凭证. 正确的 IAM 凭据管理不在本文讨论范围之内,但是 AWS 会提醒您,不建议使用 root 凭据,并且应该违反其最佳做法. 随意遵循最佳实践并使用自定义 IAM 用户的凭据,这将是两个相同的凭据(密钥 ID 和密钥). 充分了解AWS 中的 IAM 最佳做法是一个好主意. 我们需要将以下凭据添加到 GitLab:

  1. 登录您的 AWS 账户并转到安全凭证页面
  2. 单击访问密钥部分,然后创建新的访问密钥 . 创建密钥并保留 ID 和秘密,以后需要它们

    AWS Access Key Configuration

  3. 转到您的 GitLab 项目,单击左侧栏中的设置> CI / CD .

  4. 展开变量部分

    GitLab Secret Config

  5. 添加一个名为AWS_KEY_ID的密钥,并将步骤 2 中的密钥 ID 复制到” 值”字段中

  6. 添加一个名为AWS_KEY_SECRET的密钥,并将步骤 2 中的密钥机密复制到” 值”字段中

Deploy your game with GitLab CI/CD

要部署构建工件,我们需要在 Shared Runner 上安装AWS CLI . Shared Runner 还需要能够通过您的 AWS 账户进行身份验证以部署工件. 按照约定,AWS CLI 将寻找AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY . GitLab 的 CI 为我们提供了一种使用deploy作业的variables部分传递在上一节中设置的variables的方法. 最后,我们添加指令以确保only在推送到master时进行部署. 这样,每个分支仍然通过 CI 运行,并且只有合并(或直接提交)到 master 才会触发管道的deploy工作. 将它们放在一起可获得以下内容:

  1. deploy:
  2. stage: deploy
  3. variables:
  4. AWS_ACCESS_KEY_ID: "$AWS_KEY_ID"
  5. AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET"
  6. script:
  7. - apt-get update
  8. - apt-get install -y python3-dev python3-pip
  9. - easy_install3 -U pip
  10. - pip3 install --upgrade awscli
  11. - 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
  12. only:
  13. - master

确保在最后一个脚本命令中更新区域和 S3 URL 以适合您的设置. 我们的最终配置文件.gitlab-ci.yml看起来像:

  1. image: node:10
  2. build:
  3. stage: build
  4. script:
  5. - npm i gulp -g
  6. - npm i
  7. - gulp
  8. - gulp build-test
  9. cache:
  10. policy: push
  11. paths:
  12. - node_modules/
  13. artifacts:
  14. paths:
  15. - built/
  16. test:
  17. stage: test
  18. script:
  19. - npm i gulp -g
  20. - gulp run-test
  21. cache:
  22. policy: pull
  23. paths:
  24. - node_modules/
  25. artifacts:
  26. paths:
  27. - built/
  28. deploy:
  29. stage: deploy
  30. variables:
  31. AWS_ACCESS_KEY_ID: "$AWS_KEY_ID"
  32. AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET"
  33. script:
  34. - apt-get update
  35. - apt-get install -y python3-dev python3-pip
  36. - easy_install3 -U pip
  37. - pip3 install --upgrade awscli
  38. - 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
  39. only:
  40. - master

Conclusion

演示存储库中,您还可以找到一些样板代码来使TypeScriptMochaGulpPhaser与 GitLab CI / CD 完美地结合在一起,这是在制作Dark Nova 时汲取的教训的结果. 使用免费和开源软件的组合,我们拥有完整的 CI / CD 流水线,游戏基础和单元测试,所有这些操作和部署都可以在每次熟练掌握的情况下完成-只需很少的代码. 可以通过 GitLab 的构建日志轻松调试错误,并且在成功提交后的几分钟内,您就可以在游戏中实时看到更改.

借助 Dark Nova 从一开始就设置持续集成和持续部署可实现快速而稳定的开发. 我们可以轻松地在单独的环境中测试更改,如果需要,可以在多个环境中测试更改. 平衡和更新多人游戏可能会持续且乏味,但是相信使用 GitLab CI / CD 进行稳定部署会在快速获得玩家更改方面有很大的喘息空间.

Further settings

以下是一些可以进一步研究的想法,可以加快或改善您的流程: