ドメイン駆動設計の値オブジェクトとエンティティについて学んだことをまとめる(TypeScript)
『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』を最近読んでまして、そこで学んだことを自分なりにまとめたいと思います。ドメイン駆動設計についての知識はまだ生半可なものなので、間違いはあるかもしれません。そのときは指摘よろしくお願いします。
コードで示すときに使う言語は TypeScript
です。また、動作はユニットテストで確認します。
ドメイン駆動設計についてはここでは話しませんが、短く説明するなら、そのシステムについての知識(ドメインの知識)に焦点をあててソフトウェアを設計する手法です。
今回は、ドメイン駆動設計(以下、DDD)の基本要素(パターン)、値オブジェクトとエンティティについてまとめます。
概要
値オブジェクト
は、オブジェクトを値として表現するパターンです。DDD においては、ある概念を値オブジェクトで表現することで設計、実装がしやすくなります。
また似たようなパターンとしてエンティティ
があります。これも概念をエンティティというパターンで表現することで設計、実装がしやすくなります。
両者は似ており、違いとしては、値オブジェクトはオブジェクトが不変であること。同じかどうかは、そのオブジェクトが持つデータの値が全て同一かどうかで判断することです。エンティティは、可変であること。同じかどうかは、識別子(ID)が同一かどうかで判断します。
値オブジェクトについて
値オブジェクトとは、オブジェクト(クラスのインスタンス)を値と同じように扱うパターンです。値は、1 や 2 のような数値や "hoge" のような文字列の値のことです。このパターンは、インスタンスを値と同じように扱うということです。
値オブジェクトは以下の性質を持っています。
- インスタンスが持つデータは変更できない(不変である)
- 変数に代入が可能(交換が可能)
- 比較が可能(等価性による比較)
例えば、あるシステムに"氏名"という概念があり、それを値として表現したいとします。 コードで表すと以下のようになります。
export class FullName { constructor(private readonly firstName: string, private readonly lastName: string) {} equals(other: FullName): boolean { if (this == other) { return true; } return ( this.firstName === other.firstName && this.lastName === other.lastName ); } toString(): string { return `${this.lastName} ${this.firstName}`; } }
実際の使い方は以下のようになります。
import { expect } from "chai"; import { FullName } from "./full-name"; describe("FullName のテスト", () => { it("名前には名字、名前がある", () => { const name = new FullName("taro", "hoge"); expect(name.toString()).equal("hoge taro"); }); it("インスタンスが持つデータは変更ができない", () => { const name = new FullName("taro", "hoge"); // "プロパティ 'firstName' はプライベートで、クラス 'FullName' 内でのみアクセスできます。" というエラーが出る name.firstName = "mitsuru"; }); it("変数に代入が可能", () => { let name = new FullName("taro", "hoge"); name = new FullName("mitsuru", "hoge"); const name2 = name; expect(name2.toString()).equal("hoge mitsuru"); }); });
ふるまい
値オブジェクトにふるまい(メソッド)を持たせることで同じロジックが散らばるのを防ぐことができます。先程の FullName
の例で示すと以下のようになります。
export class FullName { constructor(private readonly firstName: string, private readonly lastName: string) {} equals(other: FullName): boolean { if (this == other) { return true; } return ( this.firstName === other.firstName && this.lastName === other.lastName ); } full(): string { return `${this.lastName} ${this.firstName}`; } }
full()
メソッドが追加されました(というか toString()
を full()
に変更しました)。このふるまいを使って氏名を文字列として取得したいとき、full()
メソッドを使えばその表現をすることができるようになります。
例えば、学生の名簿という概念があり、その概念には学生の氏名と電話番号を登録したいとします。これを値オブジェクトを使わない場合と使った場合でどう違いがあるのかを例で示します(あまり良い例ではない気がしますが)。
(名簿は英語で roster
と言うそうです。また name list
とも言うそうです)
値オブジェクトがない場合
export class Roster { // 氏名と電話番号を紐付ける private names: Map<string, string> = new Map(); get length(): number { return this.names.size; } regist(fullName: string, phoneNumber: string): void { this.names.set(fullName, phoneNumber); } // 氏名から電話番号を取得する findPhoneNumber(fullName: string): string | null { const found = this.names.get(fullName); return found ? found : null; } }
import { expect } from "chai"; import { Roster } from "./roster"; describe("Roster のテスト", () => { describe("値オブジェクトを使わない場合", () => { it("氏名から電話番号を取得する", () => { const roster = new Roster(); const fullName = "Hoge Taro"; roster.regist(fullName, "090-XXXX-XXXX"); expect(roster.findPhoneNumber(fullName)).equal("090-XXXX-XXXX"); }); }); });
roster
に氏名と電話番号を登録するときに文字列型の氏名を用意し、登録します。そのあと、同じ氏名の値を使って電話番号を取得しています。
値オブエジェクトがある場合
まず、学生の概念を表す Student
を用意します。
export class Student { constructor(private readonly firstName: string, private readonly lastName: string) {} equals(other: Student): boolean { if (this == other) { return true; } return ( this.firstName === other.firstName && this.lastName === other.lastName ); } fullName(): string { return `${this.lastName} ${this.firstName}`; } }
この Student
を使って、名簿を実装します。
import { Student } from "./student"; export class Roster { // 氏名と電話番号を紐付ける private names: Map<string, string> = new Map(); get length(): number { return this.names.size; } regist(student: Student, phoneNumber: string): void { this.names.set(student.fullName(), phoneNumber); } // 氏名から電話番号を取得する findPhoneNumber(student: Student): string | null { const found = this.names.get(student.fullName()); return found ? found : null; } }
値オブジェクトを使わない場合と同じテストを行います。
import { expect } from "chai"; import { FullName } from "./full-name"; import { Roster } from "./roster"; describe("Roster のテスト", () => { describe("値オブジェクトを使う場合", () => { it("氏名から電話番号を取得する", () => { const roster = new Roster(); const student = new Student("Taro", "Hoge"); roster.regist(student, "090-XXXX-XXXX"); expect(roster.findPhoneNumber(student)).equal("090-XXXX-XXXX"); }); }); });
学生を表す Student
が氏名の情報を持つようになったので、Student
の値を渡すだけで済むようになりました。また、値オブジェクトがない場合は、文字列を渡していたので学生以外の氏名を渡せるようになっていましたが、値オブジェクトがある場合では、Student
のみしか受け付けないため、学生以外の氏名が紛れないようになりました(コンパイルエラーとして検出される)。
さらに今回の実装では、名簿に情報として氏名を利用していますが、氏名だと同姓同名の場合に上手くいかないため仕様を変更することになり、ID を用意することになったとします。そのときは、Student
に ID の情報を追加し、Roster
からは ID を取得するように実装を変更すれば対応できます(本当は StudentId
と Student
を紐付けるような名簿にしたほうがいいのでしょうが)。つまりこれは、変更に強いということを意味します。
個人的には、値オブジェクトを使うことで間違った情報を引数に渡してしまうエラーがコンパイル時に検出できるのが良いと思っています(これは DDD に限らないオブジェクト指向を理解している人なら当たり前のことなのかもしれませんが)。
以上、値オブジェクトパターンの話でした。値オブジェクトは DDD に関係なく便利なパターンだと思います。ですが、(ドメインの)概念をよく理解せずに使っても無駄な値オブジェクトが増えてしまうことになるので、ドメインの理解をし、それをオブジェクトに適用していくことが大事なのだと思います(それが難しい)。
エンティティについて
エンティティは、値オブジェクトに似ています。でも、その性質は逆です。値オブジェクトは同じという意味は、値オブジェクトが持つデータの内容がすべて同一であれば同じです。エンティティはそうではなく、ある識別子が同じであれば同一とみなします。
そのインスタンスが生きている間に(識別子以外の)データの値が変わるような概念であれば、それはエンティティとして扱うべきです。
エンティティには以下の性質があります。
- 値の変更が可能
- 同じかどうかは識別子で判断される
値オブジェクトでは、不変でしたが、エンティティでは可変です。また値オブジェクトでは"同じ"の意味は持っているデータの値が同じであれば同じでしたが、エンティティでは、そうではありません。識別子つまり ID のことですが、これが同じであれば同じということです。
実際にコードで示すと以下のようになります。
export class Student { constructor( private readonly id: string, private firstName: string, private lastName: string ) {} equals(other: Student): boolean { if (this == other) { return true; } return this.id === other.id; } fullName(): string { return `${this.lastName} ${this.firstName}`; } }
import { expect } from 'chai'; import { Student } from './student.entity'; describe('Student のテスト', () => { it('同じかはどうかはIDで比較される', () => { const student1 = new Student('aaa', 'Taro', 'Hoge'); const student2 = new Student('bbb', 'Taro', 'Hoge'); expect(student1.equals(student2)).false; }); });
ここでは省略しますが、エンティティでも値オブジェクト同じようにふるまいを持たせることで表現力を増やします。エンティティの特徴は、生きている間は ID 以外の値が変更される可能性があるということとエンティティ同士の ID が同じであれば、同一であるとみなされることです。 例えば、人の氏名はエンティティであると言えます。これはその人が生きている間は氏名の変更が可能であり、その氏名が変わったとしてもその人自体は前の氏名と同一人物だからです。 概念にこういった性質があれば、それはエンティティとして扱うべきでしょう。
それ以外の性質、特徴については値オブジェクトと同じです。この二つは似ているけど異なるので一緒に学んで比較することでお互いを理解していくのが良いと思います。
値オブジェクトと比べると簡単になってしまいますが、以上です。
おわりに
DDD の初歩的なパターンとして値オブジェクトとエンティティを紹介しました。値オブジェクトとエンティティを初めて知ったときは少し混乱したのですが、値オブジェクトが値だと理解すればそんなに難しくはないのかなと思います。値オブジェクトがどういったものか分からなくなったら string のようなものだと覚えるといいかもしれません。string は文字列という概念を表現するための値オブジェクトです。
結局の所、パターンを使えば良いプログラミング(設計と実装)ができるようになるわけではなく、 ドメインの理解が不可欠なのでそこをサボらないように注意したいです。
2020年を雑に振り返る
雑に振り返ります。
今年の目標
立てた目標は次のようです。
記事として書いたことをすっかり忘れてました。
pickles-ochazuke.hatenablog.com
どれだけ達成できたのか楽しみですね()。
AtCoder を始める
してません。何を言っても言い訳にしかならないと思うのでこれしか言えません。
本をたくさん読む
たくさんの定義をしていませんが、内容を見ると月1冊読むことだったようです。 月に1冊は読んでないですが、「エンジニアの知的生産術」という本を読んで勉強の仕方や本の読み方の意識が変わったので良かったです。
この本を読んだ影響で、本は最後まで全部読まなくてもいい、というのが自分なりに理解出来ました。 今は、「まえがき」、「目次」を読んで概要を知り、目次で読みたいところにチェックを入れて、読む順番もなるべく著者が読んで欲しい順番を意識しつつも、興味があるところから読むようにしたり、まずは各章をさらっと流し読みしたりと自分が飽きない、理解しやすい方法で読めんでいます。
ドメイン駆動設計(DDD)関係の本はこの方法で追いかけています。「現場に役立つ設計の原則」を初めに読み始めて、途中でコードレベルで読みたくなったので「ボトムアップのドメイン駆動設計入門」を読み始めて、参考書もチラ見したりしています。今はボトムアップの本も少し理解できなくなってきているので、実際にコードを書いたり、エリック・エヴァンスの本もつまみ食いしていこうかと考えています。 DDDについてはまだ理解が浅いですが、飽きずに続けられているのでこの読み方は成功していると思います。
あと Kindle Oasis を買いました。これのおかげでお風呂内で読めるようになり、それによって読書が進んでいる気がします。上記のDDDの本とか特に。
他に読んで良かった本は、「プログラミング TypeScript」です。これを読んで TypeScript で書くのが楽しくなってきました。
「レガシーコード改善ガイド」や「リファクタリング 2版」を少し読みましたが良書として有名なだけあり良いです。もっと早く読んでおけばと思いました。まだ1章程度しか読んでませんが、自分が気になったとこだけでも読みたいです。
トランペットを始める
始めてません。
551 の豚まんを食べる
食べてません。
スノボーをする
2月ごろにしました。めちゃくちゃコケてケツが割れるかと思いましたが、とても楽しかったです。あと両親指の爪が根本からもげました。今は7割ぐらい生えてきており、あと少しで元の爪の長さに戻りそうです。爪がもげた原因は指を曲げながら履くような感じで靴のサイズが合っておらず、その状態で滑っていたらテコの原理でもげました。まだ治っていないので来年は滑りません。
Android アプリを作れるようになる
同人誌を書きました。あとは、Ionic でアプリを作り始めています。
同人誌の話はこれ。
pickles-ochazuke.hatenablog.com
雑な内容ですが、とりあえず書き上げて販売した自分を褒めたい。でも売れるたびに申し訳ない気持ちが出たので、次は自信を持って人に読ませられる内容にしたい。 もう書いたときのことを覚えていないですが、記事を見ると思うところは多々あったようで、次書くとき見直したほうが良さそうです。
「数学文章作法 基礎編」という本を読んで良かったので、次書くときはこの本を参考に書きたいです。
Ionic は自分の持っている技術とマッチしており、いい感じです。Angular が使えるというので触っています。世の中にマルチプラットフォームのためのフレームワークはたくさんありますが、自分はこれが初めて肌が合った感じがします。
まとめ
とくに意識して行っているわけではなかったですが、今年はいろいろ新しいことに手をつけた気がします。
- ドメイン駆動設計
- Ionic
- 同人誌即売会の参加(出す側で)
- 電子書籍の出版(技術書展)
- スノボー
- スキレットを買って料理した
- ルンバを買った(全然使わずじまい)
- ホットケーキをホットケーキミックスから作った
- VRChat を触った(外人と少し会話(意思疎通?)した)
- 初めてボランティアに参加した
- 3年くらい前から繋がらなくなってた指の皮を皮膚科で診てもらって治療を始めた(単なるイボだった)
- Google アナリティクスをブログに入れてみた(最初に設定してから放置してる)
振り返ると先月ぐらいの話かと思ってたけど半年ぐらい前の出来事でそんな前だっけ?!と時間が噛み合ってなくて驚いた。 4~7月あたりは仕事が忙しくて ほとんどの時間を仕事に費やしてたからかもしれない。 自分は忙しくなると、というか精神的に追い詰められると新しいこと(小さいことだけど)をしだす傾向がある。 あとは去年の今の時期に引っ越して部屋がかなり広くなったのも影響しているのかも。
部屋といえば、大掃除で部屋を片付けて今必要なものだけを残してそれ以外は空いてる部屋に置いたら部屋がスッキリしたのと同時に、空いてる部屋がモノでいっぱいになったので自分がどれだけ無駄なものを持っているか再認識した。 断捨離とかではなく単純に引っ越しやすい状態にしたいという気持ちがあるので、来年はモノを減らしていこうと思う(あと整理整頓)。
来年の抱負は、来年考える。
Akashic Engine でエンティティをクリックしたときにそのエンティティの色を変えるだけ
肉寿司がとても美味しかったので共有したいと思います。
概要
Akashic Engine で簡単なプログラムを作るの第二弾です。
前回はこちら
pickles-ochazuke.hatenablog.com
今回は、こんな感じのことをします。
エンティティをクリックしたら色が変わる。ただそれだけです。
したこと
今回は、前回よりも簡単かもしれません。
- エンティティを作る
- エンティティにクリックしたら、色が変わる処理を登録する
だけです。
エンティティを作る
generateBlock()
という関数を用意し、そこにクリックしたら色が変わるエンティティを作る処理を書きます。
// クリックしたら色が変わるエンティティを作成する function generateBlock(scene: g.Scene): g.E { const block = new g.FilledRect({ scene: scene, cssColor: "#FF0000", width: 32, height: 32, touchable: true, }); return block; }
この関数は scene.loaded
で以下のように呼び出しています。また、このエンティティをクリック可能にするために、touchable
の値を true
にしています。これをしないとエンティティがクリックされても検知できません(つまり、クリックしても何も反応しません)。
scene.loaded.add(() => { // 画面に表示するブロックを作る const rect = generateBlock(scene); // 画面の中央に来るようにする rect.x = g.game.width / 2; rect.y = g.game.height / 2; // シーンにエンティティを登録する scene.append(rect); });
エンティティにクリックしたら、色が変わる処理を登録する
この処理は、generateBlock()
で行います。
function generateBlock(scene: g.Scene): g.E { const block = new g.FilledRect({ scene: scene, cssColor: "#FF0000", width: 32, height: 32, touchable: true, }); block.pointDown.add(() => { // 色の状態を変化させる switch (block.cssColor) { case "#FF0000": block.cssColor = "#00FF00"; break; case "#00FF00": block.cssColor = "#0000FF"; break; case "#0000FF": block.cssColor = "#FF0000"; break; default: block.cssColor = "#FF0000"; break; } // これを実行しないと見た目が変わらない block.modified(); }); return block; }
block.pointDown.add(...)
が追加した処理です。pointDown
がクリックしたときのイベントです。イベントはいろんな種類があり、scene.loaded
もその一つです。他には、pointMove
や pointUp
があります。先にも書きましたが、イベントに処理を登録しても touchable
を true
にしないとここで登録した処理は行われません。
イベントの話は、公式ドキュメントにもあります。
今回はとても簡単な内容でしたが以上です。
コード全文
// クリックした対象の色を変えるプログラム function main(param: g.GameMainParameterObject): void { const scene = new g.Scene({game: g.game}); scene.loaded.add(() => { // 画面に表示するブロックを作る const rect = generateBlock(scene); // 画面の中央に来るようにする rect.x = g.game.width / 2; rect.y = g.game.height / 2; // シーンにエンティティを登録する scene.append(rect); }); g.game.pushScene(scene); } // クリックしたら色が変わるエンティティを作成する function generateBlock(scene: g.Scene): g.E { const block = new g.FilledRect({ scene: scene, cssColor: "#FF0000", width: 32, height: 32, touchable: true, }); block.pointDown.add(() => { // 色の状態を変化させる switch (block.cssColor) { case "#FF0000": block.cssColor = "#00FF00"; break; case "#00FF00": block.cssColor = "#0000FF"; break; case "#0000FF": block.cssColor = "#FF0000"; break; default: block.cssColor = "#FF0000"; break; } // これを実行しないと見た目が変わらない block.modified(); }); return block; } export = main;
WSL で Ionic プロジェクトを Android 実機にインストールする手順
Ionic を触っていて、Android プロジェクトを作ったはいいけど wsl からどうやって実機にアプリをインストールしたらいいんだろうと四苦八苦しました。その手順です。
Android Studio 使えるならその環境を使ってビルド、インストールしたほうが楽だと思います。
環境
以下のような環境で行いました。
アプリ | バージョン |
---|---|
WSL2 Ubuntu | 18.04 |
Node.js | v14.4.0 |
NPM | 6.14.8 |
Ionic | 6.11.8 |
概要
大まかな流れは以下のような感じです。Ionic のプロジェクトはすでに作られている前提とします。
- JVM をインストールする
- Android ツールを入れる
- Android ツールのパスを通す
- Android ツールを使って必要なツールを入れる
- Windows の方で adb.exe のパスを通す
- adb.exe のエイリアスを wsl 側に登録する
- 実機を開発者モードにする。USB接続を有効にする
- 実機を接続し、Windows 側で
adb devices
を実行し接続する - Ionic プロジェクトから Android プロジェクトを生成する
- Android プロジェクトに移動して、
gradlew assembleDebug
を実行する - apk ファイルがあることを確認し、
adb -d install app/build/outputs/apk/debug/app-debug.apk
を実行する
詳細
では、各項目を順番にしていきます。
JVM をインストールする
JVM のインストールはいろんな記事があるので、そちらにお任せします。例えば、次のような記事の手順でインストールできます。
https://qiita.com/mitsu48/items/0f18c62a9e368752b243
JAVA_HOME
の設定も必要です。
Android ツールを入れる
ツールを置いておく場所はどこでもいいです。あとでパスを通します。Android SDK ツールは、以下から落としてきます。
https://developer.android.com/studio/#downloads
Linux(64-bit) を落とします。これを展開したら android-sdk
というディレクトリを作成し、そこに入れます。
参考
https://gist.github.com/fedme/fd42caec2e5a7e93e12943376373b7d0#downloading-the-android-sdk-tools
Android ツールのパスを通す
Linux に慣れていないので、パスの通し方が正しいか分かりませんが、以下のコマンドで通します。
export ANDROID_SDK_ROOT=$HOME/android-sdk PATH="$ANDROID_SDK_ROOT/tools:$ANDROID_SDK_ROOT/tools/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH" export PATH
また、ANDROID_SDK_ROOT
なのか、ANDOROID_HOME
なのか正直分かっていないですが、ここでは、ANDROID_SDK_ROOT
で行います。
ANDROID_SDK_ROOT
のパスは、先に入れた Android SDK ツールの場所になります。
Android ツールを使って必要なツールを入れる
Android SDK Manager
を使って、ビルドするために必要なツールをインストールします。
以下は、WSL 上で行うために必要な作業ですが、Android Studio が使える環境であれば、それを使ってインストールしたらいいと思います(検証してないので、間違っているかもしれません)。
Android SDK Manager
は、 sdkmanager
というコマンドで使えます。先の Android ツールのパスを通す作業で使えるようにしています。
sdkmanager --update --sdk_root=$ANDROID_SDK_ROOT sdkmanager --list sdkmanager --sdk_root=$ANDROID_SDK_ROOT "build-tools;29.0.3" "platforms;android-29" "tools"
上記では、29 という番号を指定してインストールしています。これは Android API のバージョンを表しますので、ターゲットにしたいバージョンをインストールしたらいいです。どんなバージョンがあるかは、sdkmanager --list --sdk_root=$ANDROID_SDK_ROOT | grep build-tools
のように grep
で絞れば探しやすいと思います。
最後にライセンスの同意をするために sdkmanager --licenses --sdk_root=$ANDROID_SDK_ROOT
を実行します。
Windows の方で adb.exe のパスを通す
Android Debug Bridge(adb)を使うためにパスを通します。WSL 側から実機を認識するためにWindows 側(ホスト側)の adb コマンドのパスを通します。自分の環境では、Android Studio をインストールしていたので、%HOMEPATH%\AppData\Local\Android\Sdk\platform-tools
に置いてありました。
これを Windows の環境変数設定で Path
に追加します。
参考
https://sp7pc.com/google/android/34263
https://developer.android.com/studio/command-line/adb?hl=ja
adb.exe のエイリアスを wsl 側に登録する
Windows 側の adb.exe
を使うためにエイリアスを設定します。
alias adb='adb.exe'
実機を開発者モードにする。USB接続を有効にする
Android 実機側を開発機にするために、開発者モードにします。
参考
https://qiita.com/knakamigawa/items/5b8bd2793920b517bf25
https://developer.android.com/studio/debug/dev-options?hl=ja
実機を接続し、Windows 側で adb devices
を実行し接続する
実機と PC を接続します。
WSL の場合、Windows 側で adb devices
を実行し、実機と接続します。そのあとで、WSL 側から adb devices
を実行します。
もし、先に WSL 側から実行していて、接続が上手くいかない場合、adb kill-server
を WSL 側で行ったあと、Windows 側でも行ってください。adb kill-server
接続を切断するコマンドです。
自分の環境では、Windows 側でadb devices
を実行しなくても上手くいったような気がするので、WSL側だけでいいかもしれません。
成功すると以下のような結果が出ます。
* daemon not running; starting now at tcp:5037 * daemon started successfully List of devices attached 8AJX0TULG device
Ionic プロジェクトから Android プロジェクトを作成する
今回のプロジェクトは、capacitor
を使っているので、次のコマンドで Android のプロジェクトを作成します。
ionic capacitor add android
すると、プロジェクト直下に android
というディレクトリができます。
また、今後プロジェクトでプレットフォームに依存するようなモジュールを追加した場合、次のコマンドを実行して Andoird プロジェクトに反映させる必要があります。
ionic capacitor copy android
参考
https://ionicframework.com/jp/docs/developing/android#project-setup
Android プロジェクトに移動して、gradlew assembleDebug
を実行する
android
ディレクトリに移動したあと、apk ファイルを作成するために ./gradlew assembleDebug
を実行します。
./gradlew installDebug
でインストールできるらしいのですが、WSL 上で行っているせいか上手くいきませんでした。ですので、インストールは別の方法(adb コマンドを使った方法)でインストールします。
参考
https://developer.android.com/studio/build/building-cmdline?hl=ja#build_apk
apk ファイルがあることを確認し、adb -d install app/build/outputs/apk/debug/app-debug.apk
を実行する
先の ./gradlew assembleDebug
によって、app/build/outputs/apk/debug/
に app-debug.apk
というファイルを作成したので、次のコマンドで実機にインストールします。
もし android ディレクトリ
に移動してなければ、移動してから実行してください。
adb -d install app/build/outputs/apk/debug/app-debug.apk
成功すると以下のような結果が出ます
Performing Streamed Install Success
実機にインストールされているか確認し、動かしてみましょう!
参考
https://developer.android.com/studio/build/building-cmdline?hl=ja#RunningOnEmulator
おわりに
CUI(コマンドライン) で行うことに理由がなくて、 Android Studio 使えるならそれを使ったほうがいいです。
Akashic Engine で 敵を出して、クリックしたら弾が飛んで敵を倒すだけ
最近、Akashic Engine というライブラリで遊んでいます。
Akashic Engine
がどういうものかというのは、公式に詳しく書いてあるのでここでは説明しませんが、簡潔に書くとどんなプラットフォームでも使えるゲームエンジンです。ドワンゴさんが開発しています。
ゲームを作って公開してみようかと考えていましたが、モチベーションの維持や学んだことを記録するためにできたことを記事に出力していこうかと思います。Akashic Engine を使ってニコ生ゲームを作ってみたい人に向けて書こうと思っているので、なるべくコピペで済む(できる)ような内容にしようと思っています。
で、今回はタイトルにあるようにクリックしたら弾っぽいものが飛んで敵に当たったら倒すような動きのコードを書きました。自分は TypeScript で書いていますが、JavaScript しか知らなくてもなんとなく分かるんじゃないかなぁと思っています。
実行すると以下のような感じです。
プロジェクトは akashic init -t typescript-minimal
で作成しました。
したこと
コードの全文は最後に載せていますし、Github にも上げています。
今回行ったことは以下です。
- 画面をクリックしたらプレイヤーの目の前に弾を出す
- 弾は毎フレーム前に進む(今回は右に進む)
- 毎フレーム乱数を作り、値によって敵を出す
- 敵と弾が当たったら両方を消す(敵を倒す判定処理)
画面をクリックしたらプレイヤーの目の前に弾を出す
ユーザが画面をクリックしたときのイベントを登録するには、scene.pointDownCapture.add()
を使います。add()
の引数に関数を渡します。
たとえば、以下のようにします。
// 弾を管理するための変数 let bullet: g.E; // 画面内でクリックしたら弾を飛ばしたいため scene.pointDownCapture.add(() => { if (bullet == null) { // 弾を作成して、シーンに登録する(見えるようにする) bullet = shoot(scene, player); scene.append(bullet); } })
弾は毎フレーム前に進む(今回は右に進む)
今回は弾を出したいのでその処理を行っています。shoot()
関数で弾を作っています。shoot()
関数では以下のようなことをしています。
// 弾を作成する処理 function shoot(scene: g.Scene, player: g.E): g.E { const bullet = new g.FilledRect({ scene: scene, cssColor: "#0000FF", width: 20, height: 5, x: player.x + player.width, // プレイヤーの目の前に出す x y: player.y + player.height / 2 // プレイヤーの目の前に出す y }); // 弾は出現中、まっすぐ進む(毎フレーム 5pixel 進む) bullet.update.add(() => { // 画面の外に出ると消える if (bullet.x > g.game.width) { bullet.x = 0; bullet.destroy(); return; } bullet.x += 5; bullet.modified(); }); return bullet; }
shoot()
関数では弾の生成と弾の動きの設定をしています。弾の動きは
- 毎フレーム5pixel ずつ進む
- 画面外に出ると消える
を行っています。
毎フレーム乱数を作り、値によって敵を出す
敵を出現させるには以下のようにしています。
// 敵を管理するための変数 let enemy: g.E; /** * 毎フレーム以下のことを行う * 敵をランダムで出現させる(1体しか出ないようにする) * 敵と弾が出ていたら、当たり判定を行う * 判定の結果、敵と弾が当たっていたらどちらも削除する */ scene.update.add(() => { // 乱数の作成(0 ~ 9 の範囲) const value = Math.floor(g.game.random.generate() * 10); // 敵がいなくて、9 が出たら敵を出現させる if (enemy == null && value > 8) { enemy = generateEnemy(scene); scene.append(enemy); } ...
敵の出現は毎フレーム行うため、scene.update.add()
に渡す関数内で行っています。乱数の生成はMath.floor(g.game.random.generate() * 10)
で行っています。この処理は、Akashic Engine 公式のチュートリアルを参考にしました(コピペとも言う)。
敵の生成はgenerateEnemy()
関数で行っています。四角を表示したいとこに出してるだけなのでとくに説明はしません(コードは最後に載っています)。
敵と弾が当たったら両方を消す(敵を倒す判定処理)
当たり判定処理は以下のように行っています。
// 敵と弾が出現していたらあたり判定を行う if (enemy && bullet) { // 2者の位置から重なっていれば当たっていると判定する(true が返ってくる) const hit = g.Collision.intersectAreas(enemy, bullet); // 当たっていたらどちらも削除する if (hit) { bullet.destroy(); bullet = null; enemy.destroy(); enemy = null; } }
この処理は、scene.update.add()
に渡している関数内で行っています。
あたり判定はg.Collision.intersectAreas(enemy, bullet)
で行っています。intersectAreas()
には判定したいエンティティを渡します。今回だと enemy
と bullet
を渡しています。渡す引数は、CommonArea
と同じプロパティを持っている必要があります。いわゆる g.E
の派生されたものなら問題ないです。
Collision.intersectAreas()
のリファレンスは以下です。
また CommonArea
についてのリファレンスは以下です。
これらを行うとこの記事の初めに見せた GIF のような動きをするプログラムになります。
コード全文
最後にコード全文を載せておきます。
function main(param: g.GameMainParameterObject): void { const scene = new g.Scene({game: g.game}); scene.loaded.add(() => { // プレイヤーの作成 const player = new g.FilledRect({ scene: scene, cssColor: "#ff0000", width: 32, height: 32, x: g.game.width / 3, // 初期位置 x y: g.game.height - 100 // 初期位置 y }); // プレイヤーでは何もしない player.update.add(() => { }); // 弾を管理するための変数 let bullet: g.E; // 画面内でクリックしたら弾を飛ばしたいため scene.pointDownCapture.add(() => { if (bullet == null) { // 弾を作成して、シーンに登録する(見えるようにする) bullet = shoot(scene, player); scene.append(bullet); } }) // 敵を管理するための変数 let enemy: g.E; /** * 毎フレーム以下のことを行う * 敵をランダムで出現させる(1体しか出ないようにする) * 敵と弾が出ていたら、当たり判定を行う * 判定の結果、敵と弾が当たっていたらどちらも削除する */ scene.update.add(() => { // 乱数の作成(0 ~ 9 の範囲) const value = Math.floor(g.game.random.generate() * 10); // 敵がいなくて、9 が出たら敵を出現させる if (enemy == null && value > 8) { enemy = generateEnemy(scene); scene.append(enemy); } // 敵と弾が出現していたらあたり判定を行う if (enemy && bullet) { // 2者の位置から重なっていれば当たっていると判定する(true が返ってくる) const hit = g.Collision.intersectAreas(enemy, bullet); // 当たっていたらどちらも削除する if (hit) { bullet.destroy(); bullet = null; enemy.destroy(); enemy = null; } } }); scene.append(player); }); g.game.pushScene(scene); } // 弾を作成する処理 function shoot(scene: g.Scene, player: g.E): g.E { const bullet = new g.FilledRect({ scene: scene, cssColor: "#0000FF", width: 20, height: 5, x: player.x + player.width, // プレイヤーの目の前に出す x y: player.y + player.height / 2 // プレイヤーの目の前に出す y }); // 弾は出現中、まっすぐ進む(毎フレーム 5pixel 進む) bullet.update.add(() => { // 画面の外に出ると消える if (bullet.x > g.game.width) { bullet.x = 0; bullet.destroy(); return; } bullet.x += 5; bullet.modified(); }); return bullet; } // 敵を作成する function generateEnemy(scene: g.Scene) { const enemy = new g.FilledRect({ scene: scene, cssColor: "#00FF00", width: 32, height: 32, x: g.game.width - 100, // プレイヤーの正面に表示 x y: g.game.height - 100 // プレイヤーの正面に表示 y }) return enemy; } export = main;