Unity 3D/2Dゲーム開発実践入門の電子版が発売されます。無料試読版も配布!

先日発売・紹介させていただいたUnityの入門書 「Unity 3D/2Dゲーム開発実践入門 Unity2019対応版」の電子版が3/6に発売されることになりました! 紙の書籍と合わせて、こちらもよろしくお願いいたします。

Unity 3D/2Dゲーム開発実践入門 Unity 2019対応版

Unity 3D/2Dゲーム開発実践入門 Unity 2019対応版

また、本書の構成や内容をもっと知ってもらうために、 無料試読版をPDFにて配布させていただくことになりました! 以下からダウンロードができます。

bit.ly

無料試読版には以下の内容が含まれています。

  • はじめに〜目次
  • Chapter3-4:スクリプトによる重力の操作
  • Chapter4-6:エフェクトの表示(冒頭のみ)
  • Chapter5-3:3Dプレイヤーキャラクターの構築
  • Chapter6-3:アセットのインポートと設定
  • Chapter7-2:AndroidのKeystoreの作成

f:id:miki05:20200303091603p:plain f:id:miki05:20200302230528p:plain

こちらの試読版により、より多くの人に本書とその雰囲気を知ってもらえたらいいなと思っています。 ぜひ、お知り合いの方に広めていただけると幸いです。

Unity入門書の改訂版を書きました。

この度、Unityの入門書「Unity 3D/2Dゲーム開発実践入門 Unity 2019対応版」を執筆しました。 こちらは以前執筆した「Unity5 3D/2Dゲーム開発実践入門」の改訂版になります。 全国書店では本日2/20くらいから、アマゾンでは明日2/21に発売で、 ページ数やコラムは増量しましたが、お値段は据置でとてもお得です。

Unity 3D/2Dゲーム開発実践入門 Unity 2019対応版

Unity 3D/2Dゲーム開発実践入門 Unity 2019対応版

  • 作者:吉谷 幹人
  • 出版社/メーカー: ソシム
  • 発売日: 2020/02/21
  • メディア: 単行本

ということで、改訂を経た本書のおすすめポイント等を紹介したいと思います。

特徴1 :Unityの基礎知識に特化!体系的に学べる!

本書は、読者の方にUnityユーザーとして「ひとり立ち」してもらうことを目的にしています。 「ひとり立ち」とは、何か目標がある時に、 知っている知識をベースにいろいろ試してみたり、 適切なドキュメントを読みにいったり、コミュニティで質問したりして、 最終的には実現することが出来る状態です。 逆に「ひとり立ち」できていない状態というのは、 「何をしていいか・何から始めていいかわからない」状態ともいえます。

Unityは本当に素晴らしいゲームエンジンで機能も豊富なのですが、 それ故に覚える機能や操作がたくさんあるのも事実です。 また、ゲームというジャンルはRPG、アクション、パズル、2D、3D、VRなど、多岐に渡る技術や作り方が存在しています。 そのため、単に特定の機能やジャンルの作り方にフォーカスした説明では、 幅広い要件に対応出来る力が身につきにくいと考えています。

そこで、本書では出来る限りUnityやゲーム開発のコアとなる部分にトピックに絞り込み、 それらを、適切な広さと詳しさで体系的に説明することにしました。 C#の文法や、特定ジャンルのゲームロジックを細かく解説するよりも、 純粋なUntiyの知識の部分を450ページの中に「ギュッ」とつめこんであります。 それらも、これからUntiyと共に過ごす方(例えば自分でゲームを作ってリリースしたいとか、ゲームエンジニアとして働くとか) に必要だと思う機能を一覧化した後、ピックアップして構成を練っています。

全体の流れと紹介・利用機能を以下にざっくりまとめてみました。

f:id:miki05:20200220003720p:plain

また、発展的な内容や機能もコラムとして掲載しました。 これらのコラムでは詳しい利用方法までは紹介していませんが、 「こんな機能あるんだー」ぐらいにみてもらうことで、 いざ必要になった時のとっかかりにしてもらえたらと思っています。

特徴2:それっぽいゲームが作れる!

本書では、学習の途中で作成するサンプルゲームが「面白いこと」を重要視しました。 本書のコアコンテンツは4つのサンプルゲームをつくりながら、 Unityの機能や作法を学んでいくストーリーになっています。 4つのサンプルは、ほぼフルスクラッチで作成しますが(コードも全て誌面に掲載してあります)、 それなりにゲーム性があったり見栄えがよかったりします。 出来上がったゲームを自分のスマホに入れて、 他の人に遊んでもらいたくなる程度のものが仕上がります。

f:id:miki05:20200217003655p:plain

「面白い」を作るという、ゲーム開発はちょっと特殊な行為のように感じます。 言い方を変えると「面白い」と向き合う行為かと。 誰しもがゲーム開発をしていると「作っているこのゲームは面白いのだろうか?」と思うことがあるはずです。 それでも、開発を進めてリリースして、誰かに楽しんでもらうところまで漕ぎ釣られるのは、 「面白い」を作ることが「面白い」と感じるからだと思います。 そんな、ゲームを作るという面白さを体験してもらわなければ入門書として意味がない!という思いから、 サンプルのゲームは面白い・だけどちゃんとUnityの機能も学べるということを目指しました。

特徴3:ゼロからリリースまでアプリ開発事情をカバー!

本書のもう一つの特徴は、 スマートフォン向けの開発を全面的にカバーしているということです。 決してそれ以外のプラットフォームで参考にならないわけではないですが、 モバイルプラットフォームでリリースまでに必要なトピックをふんだんに取り入れています。

  • 実機環境構築
  • ゲームデザイン
  • インプット制御/加速度センサー
  • マルチアスペクト・解像度対応
  • リリース設定(アイコン・スプラッシュ・64bit対応)
  • 本番ビルド・ストア提出
  • セーフエリア対応
  • 動画広告の埋め込み

実際にリリースのことを考えると、 プラットフォームとの連携や事情を考慮しながら進める部分が結構あります。 これらは、あんまりまとまった情報としては無いように感じますし、 数年おきに新しい概念が登場したりしてキャッチアップが大変です。 Unityを利用すると、さっとスタンドアローン向けでexeやappをビルドすることができますが、 つまずいてしまう部分ってこういう泥臭いところだったりするんですよね。 本書は、そんな泥臭さの部分を代表的なプラットフォームかつ、 お手軽に試せるモバイルを例として、かなり実践的に扱っています。

特徴4:「これからも」Unityのスタンダードに対応!

本書はUnity2019.3をメインターゲットにしており、 比較的近年導入された「基本部分」に関わるトピックにも対応しています。

  • リリースサイクルとサプスクリプションモデル
  • Unity Hub
  • ネステッド・プレハブ
  • フラットデザイン・エディターUI

特に2018.3で導入されたネステッド・プレハブは、 全てのUnity上の作業を根本的に変化させる可能性を秘めており、 Unity2019.3でのエディターUIのデザイン変更も見た目が刷新されたインパクトある出来事でした。 本書は、数年おきに起きた全てのUnityユーザーが知るべき事柄にしっかりと追従していると言えます。

なお、タイトルには「Unity2019対応版」となっていますが、 おそらくUnity2020以降も問題なく利用できるはずです。 実は本書で紹介している物理エンジンやアニメーションなどの機能部分のトピックは、 前の版の時からあまり変更がありません。 これは、単に筆者がめんどくさくて変更しなかったわけではなく、 今も昔もUnityを使い始める上で必要な部分はそれほど違わないということだと言えます。

Unityでは、DOTSやHDRP、URP、シェーダーグラフ、Addressable Assets Systemなどなど、 どんどん新しい機能が追加されていますが、 それらはUnityの基本上に習得すべき事柄だと考えます。 本書では、単なるトレンドではなく、 今後もずっとUnityを利用していく上で最初に知るべきことに的を絞り取り上げてます。

こんな人におすすめ

以上のような特徴から、 ざっくり本書をおすすめしたい人は以下のような感じです。

  • 「Unityを触れる」から「Unityで作れる」という段階に実力を伸ばしたい人
  • これから業務でUnityを利用し始める人
  • 実際にゲームを開発してストア公開までを視野に入れている人

「プログラムに不慣れな方にはレベルが高いかも」という意見もいただいていますが、 すべての手順・コードを掲載・解説していますので、 写経を進めていけば問題なくサンプルを完成させられると思います。

f:id:miki05:20200220083728j:plain

執筆の苦労話とか

前の版「Unity5 3D/2Dゲーム開発実践入門」 は苦労した甲斐もあって、皆様からとても評価していただきました。ありがとうございます。 新卒の方から、学校の教科書でしたとか、知り合いの大手ゲーム企業で配布されたという話も聞きとても驚きました。

そういうことから、 本書の「しっかり体系的にUnityを学べる本」という立ち位置のようなものを自覚しました。 しかし2015年の発売からUnityやプラットフォームまわりの事情もだいぶ変わり、 情報がだいぶ古くなってしまったことに、ずっともやもやを感じていたのです。 そのような中で、2019年2月に本格的な改定の話をいただき作業を開始しました。

改定作業では、 最初に全体の修正内容を見繕いましたが、 サンプルゲーム自体を別ゲームには変更しないことにしました。 これは、 Unityを体系的に学ぶための機能を3章から6章にかけて重複なく利用し、 コード量を抑えながら面白いゲームにするという構造を、 緻密に設計した結果があの4つのサンプルだったからです。 ゲーム内容を変更して それらすべてのパズルを再度組み直すことはできなそうだなと...

とはいえ、サンプルゲームの方針やロジックはそのままでも、 アセットの見栄えを大幅にリファインし、 Unityの新機能などへの対応を果たし、 手順や説明もより分かりやすくなるように検討を重ねました。

そんなこんなで、 サンプルゲーム自体は変わっていないですが、 Unity新機能の検証や、紹介している機能の確認、 操作フローや説明自体のリファイン、 さらには2019年内で、どんどん更新されていくUnityの情報への対応などで、 丸々1年間かかったのでした。。。しんどかった

ただ、Unity Hubや2018.3のネステッドプレハブ、 2019.3のUIの刷新などの情報を含めて改定を実現できたのは、 とても良いタイミングだと思っています。

さて、本書最大のバージョンアップ点を最後に....

f:id:miki05:20200216190209p:plain

6章のアザラシの解像度が爆上がり!! ファミコン→プレステぐらいの進化はあるでしょう。

謝辞

サンプルのリファイン、カバー書き下ろしにご協力いただいたGaryuさん、いつもの事ながらありがとうございました。 また、担当のソシム木津様、締め切り間際等でも色々無茶なお願いを聞いてくださり、ありがとうございました。

そして、家事の負担とアザラシのイラストに協力してくれた妻に感謝。

ElectronとReactでAPI叩いて何か表示するアプリを試してみたメモ

先日、会社の業務外開発イベントに参加してきた。 これは、自分が新卒の頃からやっているイベントの後継で、 テーマ自由でもくもく作業するハッカソン、所謂もくもくそん。

今回は、ずっと昔から気になっていたElectronとReact.jsを使ってみることにした。 大半はJavaScript力を向上したいという思惑がある。

作るもの・使うもの

Redmine上の自分のタスクを、取得してきて表示するだけのアプリ。

ViewのフレームワークReact.jsを利用して、 自社の Redmine Rest APIのRestAPIを叩いて表示。 それらをElectronでアプリ化する。

Vue.jsとどっち試そうか迷ったが、 なんかいろいろ応用が効きそうなReact.jsにしてみることに。

Step0: nvm, node Install

まずnvmをいれる

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.1/install.sh | bash

そして、とりあえずnodeの安定版いれる

$ nvm install --lts
$ nvm use --lts

Step1: Scaffold

React.jsのテンプレートプロジェクトをcreate-react-appでつくる

こちらの手順を参考させていただいた。 https://qiita.com/chibicode/items/8533dd72f1ebaeb4b614

create-react-appインストール

$ npm install -g create-react-app

プロジェクト生成

$ create-react-app redmine-desktop

できたプロジェクトはすでにgitリポジトリになってる。楽。

Step2: Electron化

こちらの手順を参考にさせていただいた。というよりほぼそのままの手順。

https://medium.com/@impaachu/how-to-build-a-react-based-electron-app-d0f27413f17f

以下パッケージを追加

$ npm install cross-env electron-is-dev

以下パッケージをdev環境に追加

$ npm install -D concurrently electron electron-builder wait-on

以下electron.jsをPublic以下に配置

const electron = require("electron");
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const path = require("path");
const isDev = require("electron-is-dev");
let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({ width: 320, height: 640 });
  mainWindow.loadURL(
    isDev
      ? "http://localhost:3000"
      : `file://${path.join(__dirname, "../build/index.html")}`
  );
  mainWindow.on("closed", () => (mainWindow = null));
}

app.on("ready", createWindow);
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  if (mainWindow === null) {
    createWindow();
  }
});

以下をpackage.jsonに追加

...
"description": "",
"author": "Mikito Yoshiya",
"build": {
    "appId": "net.mikinya.redmine-desktop"
},
"main": "public/electron.js",
"homepage": "./",
...

package.jsonのscripts項目を以下に変更

...
"scripts": {
    "react-start": "react-scripts start",
    "react-build": "react-scripts build",
    "react-test": "react-scripts test --env=jsdom",
    "react-eject": "react-scripts eject",
    "electron-build": "electron-builder",
    "release": "yarn react-build && electron-builder --publish=always",
    "build": "yarn react-build && yarn electron-build",
    "start": "concurrently \"cross-env BROWSER=none yarn react-start\" \"wait-on http://localhost:3000 && electron .\""
},
...

この時点で

$ npm run start

でアプリが開発モードで起動するように

$ npm run build

でapp化してdist以下にappと配布用のdmgを出力できるようになった。

Step3: Presentation, React Component構築

ビューのレイヤーを修正

公式のチュートリアル等を参考にごにょごにょいじった。

CSSも適当にbootswatchのcss.minを入れた。

cssはただ配置してApp.jsからimportしただけだけど、これでいいのだろうか。

できたReactのコンポーネントは以下みたいな感じ。

import React from 'react';

class TaskCard extends React.Component {
  render() {
    return (
      <div className="card text-white bg-primary mb-3" >
        <div className="card-header">{this.props.title}</div>
      </div >
    );
  }
}

export default TaskCard;
import React from 'react';
import ReactDOM from 'react-dom';
import './bootstrap.min.css'
import TaskCard from './TaskCard'
import IssueUseCase from './useCase/IssueUseCase'

class App extends React.Component {
  constructor(props) {
    super(props);
    this.useCase = new IssueUseCase();
  }

  render() {
    this.useCase.getOwnIssues(issues => {
      var list = [];
      for (var i in issues) {
        list.push(<TaskCard key={issues[i].id} title={issues[i].subject} />);
      }
      ReactDOM.render(list, document.getElementById('issues'));
    });

    return (
      <div className="container">
        <div className="row">
          <div className="col-lg-12">
            <h2>Tasks</h2>
          </div>
        </div>
        <div className="row">
          <div className="col-lg-4">
            <div className="bs-component">
              <div id="issues"></div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default App;

この時点ではRestAPIからcurlで叩いたjsonをファイルで保存したデータをモックデータとして、Viewに表示してみる。記法やお作法の正しいやりかたは全然不明。

うん。これでとりあえずデモはできる。

f:id:miki05:20191113010021p:plain

Step4: DataStore, API実装

実際にAPIを叩いてサーバーのデータを取ってくるようにする。 通信と非同期の処理にaxiosをつかってみる。 XMLHttpRequstのラッパーでPromiseでイベントの制御ができるらしい。

$ npm install axios

通信部分はとりあえずトークンとかクエリとかベタがき。 あとでUI上から設定できるようにしよう...

import axiosbase from 'axios'

class RedmineApi {
  constructor() {
    this.axios = axiosbase.create({
      baseURL: 'https://url_to_redmine',
      headers: {
        'Content-Type': 'application/json',
        'X-Redmine-API-Key': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
      },
      responseType: 'json',
    })
  }

  getIssues() {
    return this.axios
      .get('/issues.json?limit=100&tracker_id=4&status_id=1&assigned_to_id=270')
  }
}

export default RedmineApi;

ここで二つの問題がでた。 一つめはCORS(Cross-Origin Resource Sharing)で、異なるドメインへの通信を制限するJavaScriptフロントのよくあるやつ。 現状の開発状態でもlocalhostで立ち上げてるぽいし、うん確かにそうなるわな。 でもAppにバンドルした場合はどうなるんだ? これは、以下のElectron側の設定で解決できた。

electron.jsのnew BrowserWindow();のパラメータに以下を追加

...
  mainWindow = new BrowserWindow({
    width: 320, height: 640,
    webPreferences: {
      webSecurity: false
    }
  });
...

参考:https://qiita.com/yasuflatland-lf/items/4f57b6dd311d4918e8f5

もう一つは会社で運用しているRedmineが自己証明書で、ERR_CERT_AUTHORITY_INVALIDエラーがでて、 XMLHttpRequst系ではそれを無視できる設定にできなかったこと。 curlではkオプションを追加するだけだが。

こちらも同様に、以下のElectron側の設定で解決。 electron.jsに以下を追加

...
app.on('certificate-error', function (event, webContents, url, error, certificate, callback) {
  event.preventDefault();
  callback(true);
});
...

参考:https://qiita.com/yuya-oc/items/2764bf7a33c751498858

完成

とりあえず動くものができた。 APIトークン埋め込み、ユーザーID埋め込みなので、完全個人しか使えない。 全然時間足りなかった。

所感

  • Electreon便利, React良さそう
  • JSの作法もっと知りたい
  • webpackとかなんちゃらとか、いろいろ覚えることある
  • TypeScript覚えたい
  • Rxを導入してみたい
  • JSでテストどう書くんだ
  • Electreon上のプロジェクトでClearn Archtectureを適用してみたい
  • Appまでビルドして配布形式までとりあえず1日で走りきれたので満足
  • たまには違ったコンテキストの作業は刺激になる
  • 割と簡単に改良できそうなので、この後少しいじってみることにする

Unity Excel Importerをv1.1にバージョンアップ

ちょっと時間があったので、 去年作ったUnity上でエクセルをインポートするプラグインをアップデートした。

github.com

Release Note

  • SerializeFieldアトリビュートをサポート(非パブリックなフィールドでも利用可能に)
  • 空のセルを基本Default Valueとして扱う
  • Excel Assetのクラス名と実際のエクセルファイル名のひも付きをオプションで変更可能に
  • インポート時にログを出力するオプションを利用可能に
  • NameSpace付きのExcelAssetにも対応
  • インポートエラー時にシート名も表示

このプラグインは、 以下のUnityプログラミングバイブルのマスターデータ構築部分を執筆する際に作成したプラグインで、 結構丁寧に考えながら設計している(つもり)。

Unityゲーム プログラミング・バイブル

Unityゲーム プログラミング・バイブル

導入の仕方は以下 qiita.com

が、しかし、Unity Excel Importerでしらべると、 大御所テラシュールさんのExcel Importer Makerも候補に表示されググラビリティが低い。 Excel Importer Makerの紹介記事は結構でたりして少し寂しい。。。

そもそも、このプラグインを作った背景には、 いくつか自分の思う思想でエクセルのインポートを扱いたいという思いと、 書籍で紹介する上では、それらの機能に責任を持ちたいと思ったからだ。

もうちょい、いろんな人に使ってほしいので紹介する。

Unity Excel Importerの思想とかメリット

以下の三つが主な特徴だとおもう。

1. シンプル&高いポータビリティ

まず、一番重要視したのは、シンプルさ。メインのソースはExcelImporter.csのみ。 一部Editor上から導入をサポートするコードもあるが、そのあたりはなくても動く。 Editor拡張を利用しない場合は、数行のコードを自前で書けばよい。

ほぼEditor拡張を使わないようにしたことで、癖がなくどんな案件でも導入しやすいはず。 Unity Editorのメニューをほとんど汚すこともないし、冗長なコードが増えることもない。

また、Excel内のテーブルのスキーマは利用者がソースコードで定義する(Enityソース)。 これにより、以下二つのメリットがうまれる。

  1. jsonの様なシリアライズ形式にそのまま移行できる
  2. エンジニアはソースをいじるだけでいい(GUIをぽちぽちしなくてよい)

1.に関しては、結構重要。 プロジェクトの開発初期では、とりあえずエクセルでマスターデータを構築しておいて、 開発が進んだあと、サーバーから取得する形に変更するなど、 データのシリアライズ形式をスイッチするとめっちゃスムーズ。 つまり、プラグインのフローや形式になるべく依存せず、 プロジェクト内の都合を極力優先できるということ。

2.に関して、意外とエンジニアはGUIの手続きを操作するのが嫌いだったりする。 手順を間違えたり、既存のソースのコピペができなかったり、独自の自動生成フローが使えないから。

2. 必要スクリプトは自分でマネージできる

テラシュールさんのExcel Importer Makerはその名の通り、 ImporterをMakeするプラグインで、 エクセルごとにツールを使って専用のImporterを、専用の配置場所に自動生成する形になる。

反面、このプラグインは完全に「Excel Importer」でインポートロジックコードの追加生成はしないし、 スキーマを定義するソースの配置等は利用者側に委ねている。 (その代わりリフレクションをバリバリつかっているけど、RunTimeではインポート済みだから問題ない)

そのため、ちょっとしたデータ構造の変更とか、 フィールド名の変更とかも気軽にできるし、削除する際も気兼ねなくできる。

3. 実プロジェクトで培われたユースケースに準拠

実はこのプラグインのもとになったのは 2011年くらいにUnityを会社で使い始めたときに、 レベルデザインを簡単にするために作られたエクセルインポートスクリプトだった。

このスクリプトがめっちゃ便利で、 いろんなプロジェクトで秘伝のタレのように使われたり改修されてきた経緯がある。 つまり、実際のプロジェクトで運用に耐えうる実績を持つフローと言えると思う。

特にコメントアウトとかEnumの利用は結構重要だとおもう。

そして、プラグイン化した今も、 新しいプロジェクトのプロトタイプとかでも利用しているし、 データ層の仮実装として相当な開発速度向上に繋がっている(上記のとおり後でスイッチも可能)。

また、都度新しいユースケースも見つかったりして、 しばらくはちゃんと対応していく予定(今回v1.1)。

まとめ

つまり、実際の利用と運用を考えて頑張って設計したプラグインなので、 是非ともつかってみてほしいということを言いたい。

あと仕様はだいぶ出揃っているから、本当はテスト書きたい。

ちなみに宣伝になっちゃうけど、 このExcel Importerを使ってライトゲーム向けのマスターデータを構築する例を 上記のUnityプログラミングバイブルで紹介しているのでよければ参考にしてみてほしい。

シンプルなNodeアプリをDocker化してElastic Beanstalkに移行してみた

DBも使わない超シンプルなnodeアプリをHerokuで管理していたが、 いつぞやからか無料枠では月終わりにスリープするように体系がかわってしまったので移行を考えていた。

とりあえず勉強もかねて、AWSでDocker化してみることに。

今回のゴール

  • HerokuのHobbyプラン7$/月よりも低コストでホスティングする
    • その代わり最低限のスペック・冗長化でよい
  • 基本的なDockerのサービス使ってみることで使いまわせる枠組みを得る
    • ミニマムなデモアプリでの利用
    • 開発環境構築とワークフローの汎用化
  • Herokuレベルな触りごごちで簡単にデプロイ・管理できるようにしておく
  • HTTPS化する

AWS上のサービスや構成の検討

とりあえず、どんな構成で行くかをちょっと使ってみたりして比較

今回はコストが最優先。 なので、インスタンスはt3.nano1台くらいでいきたい。 ロードバランサーはいらない。お遊びでやるには高い(立てとくだけで月2000円くらいか)。

これじゃDockerのメリットなくないって? 現状はあまり無いが、負荷が増えた時にスケーリングできる構成に柔軟に変えられる点がいいかも。

次はどんなサービスを使うか。以下検討

  • Fargate
  • ECS
    • 細かく設定をできそうだが、その反面自前でいろいろやる必要が出てきそう。今回は生で扱うのはオーバースペック感。特に構成管理やデプロイフロー周りをしっかり検討する必要がありそう。
  • EKS
    • 未調査。Kubernetesは今後のメインストリームに?今回はまずシンプルな構成の勉強も兼ねるので一旦考慮外
  • Elastic Beanstalk
    • マルチコンテナで利用する場合ECS上で動作するが、その管理あたりを丸っと任せられる。デプロイも容易。

ということで、今回つかうもの&ざっくり構成。

  • 複雑なことしない+管理を楽にしたいのでElastic Beanstalkを使う。
  • せっかく&独自イメージをプライベートなところに置きたいのでECR(Elastic Container Registry)もつかう。
  • シングルインスタンスロードバランサーは使わない。
  • LBなしでSSL化に伴う証明書の管理がAWS上でできないので、インスタンス内にNginxのコンテナを立てそこに証明書を配置する。
  • ドメインはお名前.comで取得しているが、こちらもせっかくなのでRoute53のDNSサービスを利用することに。

Local Docker Install

そもそもMacを新調してローカルにDockerが入っていなかったので、以下を参考にインストール。 登録とかめんどかったのでbrew経由でいれた。

qiita.com

$ brew install docker
$ brew cask install docker

Node App コンテナ化

まずは、アプリ自体をコンテナ化。 いろいろ検討した結果、すこし古い記事だが、基本が抑えられていたので以下を参考にDockerfileや開発フローを構築。 postd.cc

ベースとなるImageは設定の楽さとサイズの兼ね合いでnodeのslimを選択

これでも220MBくらいになるんで本当はもっと最適化した方が良さそうだが。

以下のようなDockerfileを構築

また、Imageのbuild時にアプリケーションのソースを丸っとコピーするが、 モジュールのインストールはbuild時に行うのでnode_moduleはコピーの対象外にしておく。 (ローカルでnpm installしてた場合とか、問題は無いだろうが冗長なので)

.dockerignoreを作成

node_modules
npm-debug.log

この状態でも、 docker buildしてdocker runすればコンテナが起動してアプケーションに接続できる。

SSL化の準備・ローカルでのチェック環境

とりあえずSSL化のことについてちょっと考える。 まず証明書は無料で発行したい。ただALBは使わないので、とても楽なCertificate Managerとの連携がいまいち。

そこで以下を参考にhttps-portalを利用してみることにした。 qiita.com

基本はnginxのプロキシWebサーバーだが、Let's encryptから自動で証明書を取得・更新をしてくれる。 デモレベルのアプリであれば十分そう(自前で管理できないのはやりにくい面もあるが)

まずは、ローカルでhttps接続を確かめられる環境としてdocker-composeファイルをつくる。 ちなみに本番デプロイ前の確認用ということでdocker-compose.stg.ymlという名前にしている。

Let's encryptは証明の発行上限が週ごとにあるので、開発段階やチェック等で本番接続していると動かなくなる。 そこでenvironmentのSTAGEをlocalにしておく。

こうしておくと、オレオレ証明書を自前で発行して代わりに使ってくれる。

またローカルでhttps接続のチェック様にhostsを修正しておく

sudo vi /etc/hosts
...
127.0.0.1       mikinya.net.localhost
...

これでとりあえず、以下コマンドでImageのビルドとコンテナの起動ができる。

$ docker-compose -f docker-compose.stg.yml build
$ docker-compose -f docker-compose.stg.yml up

でブラウザからこのローカル用のドメインにアクセスした時に、https-portalとcontainerの連携のチェックができる。

開発環境・開発フロー

現状のままだとソースコードを編集するたびにdocker-compose buildしないといけないのでめんどい。 開発用のフローを別途用意する。

そもそも、node起動後ファイル更新検知・リロードが自動でできていなかったので、nodemonを導入した。 package.jsonのdependenciesにnodemonを追加

...
  "dependencies": {
    "nodemon": "^1.18.9",
...

そして、普段づかいのローカル開発環境用docker-composeを別途つくる。

こっちのポイントは以下

  • ローカル開発環境ではnginxのプロキシを利用しないでシンプルに単体で立てる
  • 起動コマンドを上書き、nodemonが起動する様にする
  • gitリポジトリローカルにあるソースをvolumesとしてマウントし、ローカルのソースを直接起動・コンテナ内の起動アプリをすぐに更新する
  • mode_modulesのvolumeを別途作成し、コンテナ内の領域からインストールしたmode_modulesを参照する

これで、package.jsonを更新後、コンテナ内からpackage-lock.jsonの更新が可能で、ローカルのリポジトリにコミットできる。しかもnode_modulesは開発環境では動的に更新が可能で、imageにビルドするときはpackage.json、package-lock.jsonできちんとインストールされる。

詳細はこちらを参考に

DockerでのNodeアプリ構築で学んだこと | POSTD

なお、Macだとファイルシステムの都合上、動作がめちゃくちゃ遅くなるという問題があるみたいだが、アプリケーションが小さいせいか、実用上全く気にならなかったのでそのままにしている。

もし問題がおきたら、docker-sync等を使うのが良いのかも。

qiita.com

まぁ、ひとまず、開発開始の流れがめっちゃ簡単になった。

  1. Macを買う、gitとdockerを入れる
  2. リポジトリをcloneする
  3. docker-compose up
  4. コード編集 5 git add & commit

以上、これで、locahostに開発環境が立ってソースの編集もできる。 npmもnode入れる必要もないし、npm installもいらない。楽

AWS準備

awsコマンドもインストールしていなかったのでインストール

brew install awscli

aws configureで適切な権限を持つユーザのAccess Key とかSecretとか入れる

また、Elastic Beanstalkのマルチコンテナを使う場合は何かしらのコンテナのレジストリサービスが必要。

シングルコンテナの場合はソースコードをアップロード後にbuildするみたいだが、 今回はNginxのコンテナもフロントに立てたいのでマルチコンテナでECRをつかってみる。

ECR

あらかじめECRのコンソールから適当な名前をつけてリポジトリを作成。

また、ゴミがたまるのも嫌だったのでLifecycle Policyも設定。

f:id:miki05:20190118171720p:plain これで3つ以上のタグが無いImageは消えるかな?(たぶん)

でローカルのビルドとpushが面倒だったのでスクリプトを作成(scripts/push-image.shに配置)。

Elastic Beanstalk準備

Elastic Beanstalkのコンソールから新しいアプリを作成。

プラットフォームをMulti Container Dockerにしておく。 とりあえずサンプルのアプリをデプロイする設定で、ロードバランサーなし、他はだいたいデフォルト。

構築が終わり提示されたURLにアクセスするとNginxが立っている様が見れる。 EC2に指定したインスタンスが立ち、EIPが振られ、ECSに項目として現れる。

Dockerrunファイル作成

Dockerrunファイルをつくる。基本docker-compose.ymlを単純に置き換える感じ。

ただ、独自にbuildしたアプリのimageの取得先がECRのURLになり、https-portalの証明書の取得を本番環境設定してやる必要がある。

Elastic Beanstalkロール権限の設定

Elastic Beanstalkの管理下でインスタンスからECRにアクセスしImageを取得できるようにしないといけない。

Elastic Beanstalkを利用し始めると、IAMにaws-elasticbeanstalk-ec2-roleが追加されているので、 こいつにAmazonEC2ContainerRegistryReadOnlyポリシーをアタッチする。

Route53設定

https-portalを利用していることもあって、おそらくDNS設定をしてからじゃ無いと証明書取得がこける気がする(未確認)。まぁ大したアプリじゃ無いので、一旦停止を良しとしDNSを最初に切り替えておく。

Route53にて新たに取得したDomainのCreate Hosted Zoneを作成。さらにCreate Record SetからRecordを追加

  • Type: A
  • Alias: Yes
  • Alias Target: Elastic Beanstalkで作成したアプリケーションURL

あとはお名前.comのDNS設定にNSレコードの情報を追加

セキュリティーグループ設定

Elastic Beanstalkがインスタンスに設定したセキュリティグループにのインバウンドにHTTPS/443を追加

Elastic Beanstalkデプロイ

さて、あとはデプロイ。基本的にコマンド一つでデプロイしたい。基本はebコマンドでできる。

ebコマンドをインストール&初期設定

brew install awsebcli
eb init

ただデフォルトのeb deployは、gitのアーカイブをアップロードする。ImageはすでにECRにpush済みなので、リポジトリのソースファイルはデプロイに必要ない。

大した容量にはならないが、必要なのはDockerrun.aws.jsonだけなので冗長なものはアプロードしたくない(s3にたまる)。

調べてみるとebコマンドはデプロイするものを任意指定でき、独自にデプロイのフローを制御できるらしい。ということで、自前でDockerrun.aws.jsonをzip圧縮してそれだけあげることに。

まずは、.elasticbeanstalk/config.ymlに以下を追加する

deploy:
  artifact: .elasticbeanstalk/artifact.zip

これでeb deployコマンドで.elasticbeanstalk/artifact.zipがアップロードされるようになる。

さらに独自のdeployスクリプトを用意する(scripts/deploy.shに配置)。

#!/bin/bash

set -eu

zip .elasticbeanstalk/artifact.zip Dockerrun.aws.json 
eb deploy

zip圧縮後deployコマンドを叩くかんじ。

ただ現状のデプロイフローはまだ問題がある

  • ERCへのpushスクリプトとdeployスクリプトを二回叩く必要がある
  • .elasticbeanstalk/config.ymlがignoreされているのでartifactの設定を他の端末でやり直す必要がある
  • gitのリビジョンとデプロイが紐づかない(Dockerrun.aws.jsonはコミット前でもデプロイできちゃう)

もろもろ考えると、さっき作ったimageをビルドしてECRにあげるスクリプトもデプロイスクリプトに組みこむといいのかも。 また、独自zipをつくるより.ebignoreファイルを作って、Dockerrun.aws.json以外は無視するほうがよいのかも。

開発とデプロイのまとめ

  • ローカル開発
    • docker-compose up
    • ブラウザからlocalhost:3000にアクセス
    • ファイルはそのまま編集して、コミットすればよい
  • Webフロントサーバー込みのローカルチェック
  • 本番デプロイ
    • scripts/push-image.sh
    • scripts/deploy.sh

目標コストに関して

さて、そんなこんなで、AWS上にアプリを移行しましたが、コストを計算。 ちなみに東京リージョンです。

  • EC2 : 0.0068USD/Hour * 24 * 30 = 4.896USD
  • EBS : 0.12USD/GB/Month (gp2) * (12 + 8)GB= 2.4USD
  • Route 53 : 0.5USD/HZ = 0.5USD
  • ECR : 0.10USD/GB/Month * 0.1GB = 0.01USD
  • EIP : 0USD
  • ECS : 0USD

合計:7.806USD

おっと...細かい費用がかさんで目標のHeroku Hobbyプラン7$より高くなってしまった...w

まぁあとは、 リージョンをオレゴンにするともう少し安くて、6.726USDでいけそう。 あとはリザーブインスタンスを使うってとこかな。

もう少し研究してみます。

Zenject InterfaceのBindにおけるIdの付け方

BindするInterfaceにIdをつける場合WithIdの書き方が限定される。 BindInterfacesAndSelfTo等を利用してまとめての指定はできなそう。

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        // できない
        Container.BindInterfacesTo<Sample>()
            .WithId(1)
            .AsCached();

        // できない
        Container.BindInterfacesAndSelfTo<Sample>()
            .WithId(1)
            .AsCached();

        // できる
        Container.Bind<ISample>()
            .WithId(1)
            .To<Sample>()
            .AsCached();
    }
}

Zenjectとかが絡むコンポーネントのUnit Test

Zenjectが絡んだときのUnit Testの例を紹介します。 あくまで例です。

例えば以下のようなHomeUseCaseというクラスのテストを行う場合を考えます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using Zenject;
using System;

public interface IHomeUseCase : IUseCase
{
    IObservable<IHomeViewModel> LoadHomeModel();
}

public class HomeUseCase : IHomeUseCase
{
    [Inject] IUserRepository userRepository;
        
    public IObservable<IHomeViewModel> LoadHomeModel()
    {
        return userRepository.Get()
            .Select(entity => new HomeViewModel(entity));
    }
}

HomeUserCaseはLoadHomeModel()をコールすると、 いろいろやってIHomeViewModelというインターフェースを取得するためのObservableを返却します。

また、HomeUseCaseはIUserRepositoryに依存しており、Injectアトリビュートで依存注入されています。

ちなみにUserRepositoryは以下のようなIOを担当するようなコンポーネントで、 通常はサーバー通信を行うコンポーネントとのインターフェースになります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using Zenject;
using System;

public interface IUserRepository : IRepository
{
    IObservable<UserEntity> Get();
}

public class UserRepository : IUserRepository
{
    [Inject] IUserDataStore dataStore;

    public IObservable<UserEntity> Get()
    {
        return dataStore.Get(); 
    }
}

この時点でHomeUseCaseをテストするためには以下の要点をどうにかする必要があります。

  1. TestコードでInjectアトリビュートやContainerをどう扱うか
  2. UserRepositoryをそのまま使うとサーバーへ通信してしまうし、そもそもHomeUseCaseのテストにフォーカスしたい
  3. UniRx使っていて非同期ぽいのをどうテストするか

解決方針

こんな状態のコードをテストをするために以下の2つの方針をたてます。

ZenjectUnitTestFixtureを使う

「1.」についてはZenjectのOptionalExtras以下にあるZenjectUnitTestFixtureを継承したクラスでテストを行うことで解決できます。 ZenjectUnitTestFixture継承したクラスでは、デフォルトでContainerを扱うことができ、 そこにテストに必要なコンポーネントをバインドしていくことでInjectアトリビュート等を扱ったテストが可能になります。

Mockフレームワークを使う

「2. 3.」に関してはMockを利用します。 Mockは本物のオブジェクトの代わりにテスト用のオブジェクトに差し替える仕組みです。 これにより、依存するオブジェクトをすべて用意する必要がなくなり、 テストしたいものだけにフォーカスできます。 また、依存するオブジェクトの状態も自由に用意できます。

ZenejctにはOptionalExtras以下にMockフレームワークのMoqが付属しているのでzipファイルを解答することで利用可能になります。

OptionalExtras以下にあるAutoMockingをダブルクリック後に展開されるファイルの中に Moq-Net35、Moq-Net46があるので、Unity Editorの状態に合わせて再度ダブルクリックします。 するとMoqディレクトリが展開されます。

f:id:miki05:20180821001353p:plain

実際のテストコード

このあたりを含め以下のようなテストコードを実装してみました。

using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;
using Zenject;
using UniRx;
using Moq;

public class HomeUseCaseTest : ZenjectUnitTestFixture // ①
{
    [Inject] HomeUseCase useCase; // ⑥
    [Inject] Mock<IUserRepository> mockRepository; // ⑥

    [SetUp]
    public void CommonInstall()
    {
        Container.Bind<HomeUseCase>().AsSingle(); // ②

        var mockRepository 
            = new Mock<IUserRepository>(); // ③
        var mockDataObservable 
            = Observable.Return<UserEntity>(new UserEntity());
        mockRepository.Setup(x => x.Get()).Returns(mockDataObservable);

        Container.BindInstance(mockRepository.Object); // ④
        Container.BindInstance(mockRepository).AsSingle();  // ⑤

        Container.Inject(this); // ⑥
    }

    [Test]
    public void CallRepositoryAndReturnsHomeViewModel()
    {
        IHomeViewModel viewModel = null; 
        useCase.LoadHomeModel()
            .Subscribe(vm => viewModel = vm); // ⑦
        Assert.IsNotNull(viewModel);
        mockRepository.Verify(x => x.Get(), Times.Once()); // ⑧
    }
}

説明箇所ありすぎなので番号を振っています

①ZenjectUnitTestFixtureの継承

テストコードはZenjectUnitTestFixtureクラスを継承します。 これでContainer等の扱いが可能になり(Baseクラスで定義)、Zenjectのテストがしやすくなります。

②CommonInstall内でのContainerへのBind

テストに必要なコンポーネントはContainerへBindしていきます。 まずはテスト対象のHomeUseCaseをバインドしています。 CommonInstallはSetupアトリビュートによりテストの前に実行されます。

③依存コンポーネントのMock生成

通常のZenjectのContainerと同様、依存するコンポーネントはContainerの中にBindしておく必要があります。 テスト対象のHomeUseCaseはIUserRepositoryに依存しているためにどうにか用意しBindします。

しかしUserRepository自体をBindすると以下の問題がでます。

  1. UserRepositoryが依存しているクラスもBindする必要がある
  2. HomeUseCaseのテストなのにUserRepositoryの状態に深く影響される

ということで、ここでMockを利用しています。 Mockを利用するためには対象のクラスにインターフェース定義が必要です。

ここではIUserRepositoryが定義されており、 HomeUseCaseでもすべてインターフェースで取り回ししているため、Mockでのテストが可能になります。

Moqの詳しい利用法は省略しますが、新しいMockオブジェクトを生成し、 Get()メソッドをスタブし、「即時UserEntityを返却するObservable」を返却するようにしています。

④MockUserRepositoryのBind

MockオブジェクトのObjectプロパティをBindすることで、Mockオブジェクトのインターフェースをbindできます

⑤Mockオブジェクト自体のBind

今回は振る舞いに関してもテストしたかったので(あとで解説)モックオブジェクト自体もBindしています

⑥テストクラス自体のBind、テスト対象のInject

最後にテストクラス自体もBindしてます。 これはちょっとしたテストコードで楽をするための工夫で、 こうすることにより、 冒頭に宣言したInjectアトリビュートのプロパティにContainer内のテスト対象のクラスがInejctされ、そのまま利用できるようになります。

⑦実際のテスト

LoadHomeModelメソッドを実際にコールし取得したObservableを稼働させることでテストを実行します。 ③で本来は非同期のObsevableを即時値を返すように設定しているので、 Subscribeをコールした直後、OnNextのストリームが流れSubscribe内のラムダに処理が移ります。 ここでIHomeViewModelがちゃんと返却されるかどうかをテストしています。

⑧UseCaseの振る舞いのテスト

一応、UseCase内でちゃんとUserRepositoryを利用しているかをチェックしています。 やろうと思えばUserRepositoryを経由せず、内部でクラスをインスタンス化しても⑦のテストは通すことはできるので。

⑧ではmockRepositoryのGetメソッドが1回呼ばれているかどうかをチェックしています。

まとめ

Zenejctとかを使う場合で依存が発生する場合のテストの例を挙げました

  • ZenjectUnitTestFixtureを使うとContainerを利用したUnit Testができる
  • MockフレームワークとInterfaceを利用することで対象のテストにフォーカスできる

正直UniRxの非同期の部分をこのようにテストすべきかどうかはちょっと不明です。 そのほかPlayModeTestを利用する方法もありますが、オーバーヘッドが大きいような?

それとMoqを利用しましたが、 MockライブラリとしてはNSubstitute(http://nsubstitute.github.io/)のほうが扱いやすい感じ。 ただMoqの方はZenjectにバンドルされており、単純なMockであればもっと簡単にかけたりします。