オブジェクト指向を完全に理解し、インターフェースを使いこなして日々プロダクトを開発している強エンジニアの方に聞きたいんですけど、生まれて始めて自力で「ここでインターフェースを作ろう!」と思った日のことを覚えてますか?
世の中にはたくさんの優秀なエンジニア・プログラマがたくさんいて、そういう方々は(もちろん技術分野によって異なるでしょうが)さも当然のように「ここでインターフェースを作ろう」と思ってプロダクトの開発を進められるのだと思います。
でも当然そういう人たちも、生まれた時からインターフェースを使えたわけじゃないと思うんですよね。
人によって経緯やその時の状況は異なると思いますが、オブジェクト指向プログラミングやカプセル化、ポリモーフィズムのことを勉強して、その後自分の開発にそれを「初めて活かせたタイミング」というのがあるんじゃないかと思うんです。
僕は開発というものを個人レベルで、また転職して会社の案件として学び始めて約3年が経ったところで、まだまだ修行中の身であります。
そんな僕は、オブジェクト指向であるとかインターフェースといったようなものは、説明を読んでその意味は分かる、既に実装されたコードがあればどういう動きをしているかなんとなくは分かる、同じように実装しなさいと言われればできる、というレベルのものでした。
ところが先日、明らかに理解のレベルが上がったな、と思うことがありました。
それがタイトルに書いた**「そうだ!インターフェースを作ろう!」と自発的に思った瞬間**です。
インターフェースがなにか、という技術的な説明に関しては、他にテクニカルな記事がいくらでもあると思うのでそちらを参考にされた方が良いでしょう。
今回僕がしたいのは単なる技術の説明ではなく、まだ自分のものになっていなかった技術を、自発的に「使おう!」と思えた瞬間の、状況や自分の中の理解についてのお話です。
ゲーム中「タップで購入できる」を示すアイコンを表示する処理
僕は個人レベルでゲームを開発することを趣味にしています。
個人でも無料で利用できるゲームエンジンUnityを使い、過去にいくつかのゲームをリリースしており、今も新しいものを開発中です。
インターフェースを作ろう!と思い立ったのは、まさにこのゲーム開発の最中でした。
その時僕は、カンタンなタワーディフェンスのようなゲームを製作中でした。
マップ上を可愛いハムスターが歩いてくるので、お菓子を飛ばす装置を通路上に設置し、お菓子を食べさせることによって満腹になって帰ってもらう、という軽い感じのものです。
その装置を設置するためには、マップ上の特定の位置をタップする必要があります。
その位置をタップした時、必要量のコインを所持していたら装置を設置できる、といった具合です。
またすでに設置した装置もさらにコインを払うことにより、性能を強化することができます。
この操作もタップによって行います。
さらにこのゲームには、全てのハムスターを緊急避難的にスタート地点に引き戻せるアイテム「ネコ」があります。
ネコは所持していれば使うことができるのですが、このネコの所持数を増やすのにもコインを使用し、画面をタップして購入する必要があります。
これらの箇所を制作していた際、ふとこう思いました。
購入に必要な量のコインが手に入ったら、「ここをタップしたら購入できるよ」ということを表示できないだろうかと。
このゲームはどちらかというとゲーム初心者をターゲットにしたようなものだったので、どこをタップしたらアクションが起こせるかを表示する機能があった方が良いと思ったのです。
実装にあたっての問題点
さっそく僕はその機能の実装に手を付け始めました。
やることは「コインがそのアクションを起こすのに必要な数貯まったら、それを示すUIを表示する」ということです。
しかしここで問題があることに気付きました。
- 装置設置ベースに装置を設置する
- 装置を強化する
- ネコを購入する
これらは全て、必要なコインの量が違うのです。
また、Unityでゲーム上に設置するオブジェクトの種類も違い、そのため条件が整った時に表示するUIの種類も違うことになります。
もう一つ、現在所持ししているコインの量は、ゲームマネージャというコンポーネントが管理しています。
これはステージのゲーム進行を統括するような役割のコンポーネントになります。
そして装置、装置設置ベース、ネコを購入するボタンに関してはゲームマネージャとは別のオブジェクトになります。
つまりゲームマネージャでコインが増減したことを契機として、各種オブジェクトが「タップしてアクション可能か?不可能か?」を調べる必要があるということです。
状況を整理すると、
- 以下のコンポーネントが別々に存在している
- コインの量を管理する「ゲームマネージャ」 ……①
- 必要コインを持っている時にタップすると装置を設置する「装置設置ベース」……②
- 必要コインを持っている時にタップすると自身を強化する「装置」……③
- 必要コインを持っている時にタップするとネコを購入する「ネコ購入ボタン」……④
- ①が管理しているコインに増減があった時、②③④のアクションに必要なコイン量を調べ、タップ可能アイコンを出し入れする必要がある
- ②③④それぞれ、アクションに必要なコイン量、タップ可能アイコンのUIオブジェクトはすべて別々
こういう要件のコードを書く必要があったということです。
#さて、どうする?
コインを管理しているのは①なので、コインが増減したタイミングは①が握っています。
なので、②③④それぞれに「アクション可能になったかどうかをチェックし、結果に応じてタップ可能アイコンを出し入れする」という処理を作り、コインが増減したタイミングで①からその処理を呼び出せば、実現できそうです。
問題は、
- アクション可能になったかどうか、という判定基準
- タップ可能アイコンオブジェクト
- ②③④の処理を書いているクラス(コンポーネント)そのもの
これらが、②③④によって異なる、ということです。
これらが異なるということは、コインが増減したタイミングで①は、
- ②のチェック用メソッド
- ③のチェック用メソッド
- ④のチェック用メソッド
を別々に呼び出す処理を書く必要があります。
……こうやって3行で書いてしまうと「イケるじゃん」的な感じがしてしまいますね。
ですがこれ、絶対に3つで済むんだったらいいんですが、あとで新たにタップ可能なオブジェクト⑤を増やしたい、という話になった場合に、⑤のチェック用メソッドをチェックさせる処理を追加しなければならず、少々面倒です。
またこれはUnityの技術的な話になってしまいますが、ゲーム中に②や③が存在している数は変動することが想定されています(④は常に1つしかないですが)。
従ってコインの増減時に②、③をゲーム中から検索し、②だったら②用のチェックメソッドを、③だったら③用のチェックメソッドを呼ぶ必要があります。
オブジェクトの配置(Unityではヒエラルキーと言います)を工夫することで多少手間を抑えられはしますが、コード的には探索やオブジェクト種の判定の処理をオブジェクトの種類分だけ書くことになるので、すごく非効率と言うか……二度手間をしているように見えるんですよね。
// たぶんこんな感じ
private void AddCoin(int addition) {
coin += addition;
foreach(Transform child in foodMachinesParentTransform) {
if (child が装置設置ベースだったら) {
②のメソッドを呼んでタップ可能かチェックする;
} else if (child が装置だったら) {
③のメソッドを呼んでタップ可能かチェックする;
} else if (child がネコ購入ボタンだったら) {
④のメソッドを呼んでタップ可能かチェックする;
} else {
なんもしない;
}
// ↑めんどい なんかコード汚い
}
}
なんとかして、コインの増減時に書く処理を、②・③・④の種類で書き分けずに、すべて同じ記述で済ますことはできないだろうか……。
そんなことを考えていた時、閃いたのです。
あれ?これインターフェースじゃね?
ITappable
というインターフェースを用意します。
/// <summary>
/// タップ可能になるオブジェクトのインターフェース
/// </summary>
interface ITappable
{
// タップ可能をチェックする処理
void CheckTappable();
// タップ不可能をチェックする処理
void CheckUntappable();
}
そしてこれを、②・③・④に実装します。
そうすると、②・③・④は共通で CheckTappable()
と CheckUntappable()
というメソッドを持つことになります。
そして、タップ可能になった際の処理を②・③・④それぞれ実装します。
このメソッドは名前は同一ですが、処理の内容は②・③・④それぞれで実装するので、異なる処理を記述することができます。
つまり、必要コイン量やタップ可能かどうかの判定基準、表示するアイコンのオブジェクトが違っていても問題ない、ということです。
その後、①が持つコイン増減のタイミングで、コインが増えた時に全てのITappable
のCheckTappable()
を片っ端から呼び出せば良いのです。
コインが減った場合は全てのITappable
のCheckUntappable()
を呼び出します。
この時、呼び出す対象のオブジェクトが②なのか③なのか④なのかは考えません。
CheckTappable()
とCheckUntappable()
は②・③・④に共通して実装されているメソッドでありながら、その内容は②・③・④それぞれで異なっているという特徴を持っているため、呼び出す側がオブジェクトの種類を判定する必要はないのです。
// こんな感じになります
private void AddCoin(int addition) {
coin += addition;
foreach(Transform child in foodMachinesParentTransform)
{
ITappable tappable = child.GetComponent<ITappable>(); // Unityでコンポーネントを取り出す処理
tappable.CheckTappable();
// ↑オブジェクトの種類とか考えずとにかく CheckTappable() を呼んでしまえ
}
}
とてもコードがスッキリしました!
インターフェースを使うことによって起こったこと
インターフェースを使ったことによって何が起こったかと言うと、
- ②、③、④に、同じ名前で処理内容の違うメソッドを作った
- そのメソッドを、①から内容やコンポーネントの種類の違いを意識せずに呼び出せるようになった
ということです。
これがインターフェースを使う利点、ということなんですよね?(>できるエンジニアのみなさん)
初めてインターフェースを使えた感
この手法を自力で思いついた時、なんか感動したんですよねー……。
今までオブジェクト指向なんかの本でインターフェースに関する記述を読んでも、書いてある文章の意味は分かるんですけど、どうしてもそれを自分で使える気がしないというか。
「ふーんなるほどねー」以上の感想を持てずにいたんですよね。
それがこの時、ようやくインターフェースというものが分かった感じがしました。
もちろんこんなのは初歩の初歩なんでしょうけど、とにかく自分にとっては「初めてのインターフェース」な経験で、ちょっとジーンとした感動があったのでどこかに書き記したい気分だったのです。
###おわり