Treasure2020に参加してきた!
はじめに
こんにちは。トコロテンです。
今年の8月にVOYAGE GROUPさんのTreasureというインターンに参加してきました。 インターンが終わった後にも忙しいイベントがたくさんあり、1ヶ月近く時間が経ってしまっていますが覚えている限り書いていこうと思います。
人生で初めてのインターンだったので参加が決まったときは嬉しさと「本当に自分はやっていけるのか」といった不安が半々くらいでした。 しかし、インターンが始まるとそんな不安は消し飛びました。 なぜなら、本気で頭を使っているとそんなことを考えている暇なんて無いからです。
「Treasureとは何か」といった問に僕が一言で答えるなら、「本気になれる営み」だったと思います。 いや、一言で答えるならですよ。 本当に一言で全てを表すことなんてまず無理です。 それほどに様々な要素があり、学びがあり、本気になれる機会がありました。
Treasureでやったこと
Treasureでは、大まかに分けて2つのことを行いました。
- 講義
- チーム開発
講義
講義は、以下の内容のものがありました。
- フロントエンド講義(JavaScript)
- バックエンド講義(Go, REST)
- バックエンド講義(Go, WebSocket, WebRTC)
- データベース講義(RDB)
- インフラ講義(AWS)
- アイデア講義
全ての講義について講義課題がリアルタイムで出題され、それを解きながら理解を深めるといったものでした。 課題は、クイズ形式のもの、プログラムを実装してプルリクを出すもの、他者と議論して考えをまとめて全体で発表するものといった様々なものがありました。 講義内容も課題もしっかり聞いたり考えたりしなければ理解できないレベルの高さだったので常に頭がフル回転していました。
チーム開発
チーム開発では、自分含め4人のインターン生と3人のサポーターさんの7人チームになって主に以下の3つのことを行いました。
- アイデア出し
- 設計&技術検証
- 実装
期間は以上の全ての工程を含めて大体2週間弱といったところでした。 正直「え、2週間あって4人で協力すれば僕たちが考えた最強のWebサービス余裕で作れちゃうんじゃね?まぁいけるっしょ笑」みたいな気持ちでした。 チーム開発が始まるまでは。 本当に浅はかな妄想でした。
チームについて
チームが発表されるとまずチーム名の決定をしました。 中々チーム名を決めたりするのが苦手なので共通点を探すといったことを行ったところ、全員お酒が好きといった共通点がありました。 僕は、特にエビスビールが好きだったので「ヱビス」という名前を提案したところ、無事チーム名は「ヱビス」に決まりました!🍺
そして、チームが決まった日の夜にチームメンバーと飲み会を行いました。 実は、この時ものすごくお腹が空いている状態でビールを飲んでいたのでとても眠くて殆ど記憶が残っていません😉
ただし、この日話した大事なことは記憶に残っています。 飲み会をしながら、チームでのスローガンのようなものと目標を語り合いました。 チームメンバー各々の目指すものは、以下のようなものがありました。
- CI/CD等のDevOpsに力を入れたい
- 発表のためだけの作品ではなく、継続して開発していけるようにしっかりと作りたい
- フロントエンドでUI/UXに力を入れたい
- 優勝したい
僕が掲げた目標は「優勝したい」といった目標でした。 「目標なんだから本気でやれるように大きな目標がいい」といった気持ちでこれを掲げました。 本当に単細胞だと思います。 しかし、チームメンバーもこの目標に賛同してくれてチーム全体でも優勝を目指すようになりました。
チームメンバーが各々の思いを語る中で「妥協したくない」といった旨のものが共通してありました。 そこで僕たちのチームのスローガンは、「妥協しない」に決定しました。 優勝を目指す上では必要不可欠だと思います。
アイデア出し
チーム開発を行う中で圧倒的に難しかったのがアイデア出しです。 冗談抜きで人生の中で一番頭を使ったと思います。 チームメンバー間でアイデア出しの期間を「暗黒時代」と言って笑ったりもしましたが、本当にその通りだったと思います。
アイデア出しの際には、以下のMiroというWebアプリを使っていました。 リアルタイムでメンバーとホワイトボードを共有することができます。
僕は普段趣味で開発を行っています。 その際にはなんとなく自分がほしいものを作ってみて使いづらかったら改善するといったことをしています。 この場合、ペルソナが自分自身であることが殆どで課題が自分の中にあり、検証を1人で行うことができます。
しかし、Treasureで創るものは「ガチ」です。 アイデア出しは、フレームワークのようなものに則って行いました。 大まかなアイデア出しの手順は以下の通りです。
- 世の中の変化を捉えて「時流」をたくさん挙げる
- サービスを開発する領域である「市場」を決定する
- 選んだ市場に存在する「プレイヤー」をたくさん挙げる
- 選んだ市場と組み合わせて価値を生み出せそうな時流を複数個選ぶ
- 選んだ時流から今後起こりうる「社会のゆらぎ」をたくさん挙げる
- 以上の情報を基に「UNS」(ユーザー・ニーズ・ソリューション)をたくさん挙げる
以上の手順で実際にアイデアを出してみるとめっっっっっっっっっっっっっっっっっっちゃ難しいです。マジで。 ここで当時の僕のツイートを見てみると…
— トコロテン (@tokoroten_lab) 2020年8月18日
はい、ハゲるくらい難しかったです。 アイデア出しの日は、毎日シャワーを浴びる前に前髪を上げて生え際がどこまで後退しているか確認してました(実話)。
暗黒時代
最後のUNSが全然出て来ないんですよね。。。 UNSのユーザの抽象度が高すぎてニーズもそれに伴ってふわっとしたものになってしまっていました。 深堀りができていなかったために、イメージがしっかりと湧くアイデアが出せていませんでした。 こうなってしまうのには、多くの要因があったと思います。
例えば、手順1の時点で挙げた時流の抽象度が高すぎたり、何が原因で起こっている時流なのかといったことがわかっていなかったりといったことがありました。 なので手順1から手順6を全て終えた後にもう一度手順1の時流を考えるところをやり直しました。
他にも、手順4で面白いアイデアが出ることを期待して市場からかけ離れた時流を選んだはいいものの、本当に市場と全く合わなくて何もアイデアが出ないといったこともありました。
さらには、選んだ市場が問題でアイデアが出ないのではないかといったことも考え、一度市場ごと変えたりもしました。
焦燥感
何度も立ち返ってアイデア出しをやり直していくうちに本当に2週間アイデア出しだけで終わるんじゃないかと思う瞬間もありました。 アイデア出しが始まって最初の2日間は、早くアイデアを決めなければという焦りの気持ちがあり、今出ているアイデアだけで無理やり決めようとする場面もありました。
しかし、サポーターさんのツッコミにしっかりと答えられるレベルのアイデアではない点、チームの「妥協しない」というスローガンの下、無理やり決めずにもっと良いアイデアを探すことにしました。
再考
アイデア講義の講師の方にアドバイスを頂き、ユーザーを固定してみたら良い、実際に存在する人にインタビューをしてみるといったことを実践してみるとユーザーの具体性が上がりました。 しかし、上記のことを実践してもまだ少しユーザーの抽象度が高いと感じていました。
ここでサポーターさんから「時間軸を考えてみると良い」といった旨の助言を頂き、これを実践してみると劇的に解像度が上がりました。 時間軸を意識することによって、ユーザーが困るタイミングや欲求のタイミングを考えられるようになって具体的なシチュエーションが思い浮かびました。
このあたりで「あ、これならいけるかもしれない」といった希望の光が見えてきました。
時間軸に沿って出したアイデアをさらに深堀り、ニーズを具体化、そのニーズを満たすソリューションを考え、「これならいける!」と思ったアイデアが出来上がりました。
ここまでで4日間かかりました。 4日間の間も本気で頭を使ってアイデア出しをするといった経験が人生で無かったため、本当に貴重な経験でした。
設計&技術検証
いざ実装!と行きたいところですが、アイデア出しが終わった状態では、必要なAPIやDBのスキーマ、画面構成等が何も決まっていない状態なのでまずはそこを決めていきます。
この設計をする際には、ユーザーがサービスを使う流れを考え、その流れの中に出てきた操作や動きを実現するために必要なAPIを生やしたりしていました。 普段の趣味の開発では、いつも以下の感じで生やすAPIが決まっていました😅
僕「この機能なんか欲しいなぁあああああ!?!?なあハム太郎!!!!お前もそう思うだろ!!!!???!!」
ハム太郎「そうなのだ!!!!!!!まったくもってその通りなのだ!」
僕「んじゃ、作っちゃいますか」
ユーザーがサービスを使う流れを考えてから設計に取り掛かる方法だと設計時に必要の無いものが含まれづらく、サービスとして成り立つ必要最低限(MVP)の機能に絞って考えることができるため、かなり画期的な方法だと感心していました。
以下は、DBのスキーマを設計する際に利用していたWebアプリです。 SQLライクな言語でスキーマをサクサク組むことができ、最終的にできたスキーマはSQLに変換することができます。 個人的にかなり好きなのでインターンで紹介してもらった後も結構使ってます。
また、設計をする前に軽く技術検証をしていました。 というのも、今回の開発では時間がとても限られていることから、外部サービスを利用して効率的にアイデアを実現していく必要がありました。 外部サービスを利用する際には、外部サービスで提供されているAPIを叩くことになりますが、そもそも自分たちのほしい機能がAPIとして提供されているのか、それをコードに落とし入れて実装できそうかといった実現可能性について調査をしていました。 検証が終わって「いけそう!問題になるとしたら時間だけだ」といった気持ちになり、いよいよ実装に入りました。
実装
設計が終わってついに実装パートに突入します。
インターンが始まる前は、インターンが始まると実装ゴリゴリのゴリラになるのかと勘違いしていたのですが実際に開発をしたのは3日程度でした。 APIのエンドポイントがしっかり定義されていたり、これまでのアイデア出しや設計でメンバーとサービスについて長い間話し合って来たことから、自分が何を開発するのかといった象が具体的で迷いが少なく、普段の趣味の開発よりも早いスピード感で開発に取り組むことができました。
ただし、開発の進め方について反省すべき点がありました。 それは、インクリメンタルな開発をしていたという点です。
以下の図は、インクリメンタルな開発とイテレーティブな開発のそれぞれを表したものになります。 これは、開発時にサポーターさんに指摘していただくまで気づくことができせんでした。
図から分かるように、インクリメンタルな開発では、1つ1つの機能を完成させていきながら最終的に全体を作っていきます。 これに対し、イテレーティブな開発では、1つ1つの機能を完成させるというより、全体像が早く見えるように「とりあえず動く」といった状態にしてから段々と細部を作っていきます。
インクリメンタルな開発では、機能が完成してから初めて共有が可能になるのでもし認識のズレがあった際にやり直しコストが大きくなったり、シンプルに全体像が見えづらいので機能を繋げていくのもどのように繋げるのかイメージが湧きづらかったりするといった欠点があります。
実際、開発時にこの問題点を抱えていて、初めの方はフロントエンドとバックエンドの連携の取りづらさを感じていました。 自分一人だけの開発では、何をするにしても自分だけが理解していれば良いのでこういった問題点は見えづらいですが、チーム開発ではそれが顕になって身にしみました。
僕が実装した箇所としては、バックエンドがZoomと連携する必要があったのでZoomのOAuth認証やZoom APIを叩く処理を主に実装しました。 OAuth認証、言葉や概念自体は前から知っていましたが、実装したことは無かったのでトリッキーな実装になってしまい無理やり実装した感がすごかったです(小並感)。 実装していく中でOAuthのフローやトークンとユーザーを結びつける方法の知見を獲得しました。
チームメンバーは、ガチプロでした。 僕がZoomの連携で手こずっている間、バックエンドを主に担当していたもう一人は無限にAPI生やして他の外部サービスとの連携が殆ど完了しているし、フロントエンドの二人も次々とページを作成してレイアウトを整い始めてるし… 本当にメンバー全員が凄くて、ここに居ることができて良かったなぁと染み染み思いました。
そして、殆どの機能ができ、機能同士を繋げていくのが爆速で行われました。 開発の最後は、自分たちの作ったサービスが動いているのを見てめちゃくちゃエモい気持ちになりました。
最終発表
インターンの最終日に成果物の発表会がありました。
発表時はとても緊張しましたが、何より他のチームの発表を聞くのがとても楽しかったです。 各チームが本気で考えて本気で作ったサービスを見せてくれるため、アイデアやそのクオリティに素直に感動するものがとても多かったです。
全ての発表が終わった後には結果発表があります。 Treasureで講義をしてくれた各分野の講師やCTOの方から以下の賞が各賞1(または0)チームに与えられます。 1チームが複数の賞を得ることもあり得るとのことでした。
チームで目指していたのは「グランプリ」でした。 そのために「妥協しない」というスローガンの下で本気で取り組んだ2週間。 結果発表の時は、心臓がバクバクしていました。
そして最終結果は…
- グランプリ
- UI/UX賞
- ニーズ賞
以上の賞を全て頂くことができました!!!
正直、初めは1つのチームに複数の賞が与えられるということはあまりなくて各チームに1つの賞が与えられるのではないかと思っていました。 そのため、UI/UX賞・ニーズ賞のどちらが先だったかは覚えていませんが1つ目の賞を与えられた際にグランプリはもう貰えないじゃないかと本気で落ち込んでました。 しかし、その後、UI/UX賞・ニーズ賞の2つの賞をもらった際に「え、これ本当に1チームが複数の賞もらえるんだ」という意外な気持ちと「だったらまだグランプリは十分ありえるんじゃないか…!?」といった気持ちで心がざわつきました。 そして、最後のグランプリが発表された瞬間はめちゃくちゃ嬉しくて疲れが全部吹き飛びました。
チームで「最終日はヱビスビールで乾杯する」という約束があったので最後に勝利の美酒を飲んで最終日は文字通り優勝しました✌
嬉しかったこと
本気になれた
冒頭でも書いた通り、Treasureには本気で取り組むことができました。
これは今思い返せば、本気になれるだけの人、環境、課題が揃っていたからこそだなぁと思います。
普段の趣味の開発って、登場人物が自分しかいなくて、代わり映えの無い環境で、自分がなんとなく不便だなぁとかこんなの欲しいなぁって思ったものを作るんですよね。 しかし、Treasureでは、めちゃくちゃレベルの高い同世代の学生や現役のガチプロエンジニアの方に囲まれて、自分の知らない新しい環境で、本気で課題を探すところから始めるんですよね。
これって刺激の塊みたいな状況でアドレナリンがめちゃくちゃ出ます。 やってやろうって気になります。
本気でものを創る経験をしたい人にはガチでおすすめのインターンです。 自分がついていけるか不安だという人も、冒頭でも述べた通り、参加してみればそんなこと考えてる暇無いのでとりあえず参加することをおすすめします。
自分だけでは気づかないことに気付かされる
Treasureでは初めて会った人と一緒に課題について議論したりチームになって開発を行うので本当に色々な考えに触れることができます。 また、講師の方やチームのサポーターさんからかなり積極的にフィードバックや指摘を頂けるので自分では気づけないような点い気づかされます。
上の方であったモナリザの図なんかがその例です。 それ以外にも他のTreasure生の課題のプルリクからコードを見て自分には無い考え方や実装を取り入れることができました。 チーム開発時には、メンバーのコードから良いところをたくさん盗ませてもらいました。
インターン全体を通して思ったこととして、Treasureでは話し合いの場がかなり多く設けられていたと思います。 それらの場があったからこそ、自分に無いものを他者から得ることがたくさんできたのだと思います。
おわりに
Treasureに関わってくださった方々、本当にお世話になりました。
課題を解決できる最強のエンジニア目指してがんばります。
また何処かで会う機会があればよろしくお願いします。
Treasure2020 3日目
はじめに
こんにちは。トコロテンです。
最近、夏だからといっても暑すぎませんか。 そう思いながら気象庁のページを見たら下の図があって目ン玉飛び出ました。
これは暑いですね。 僕の体力が落ちただけかなと少し思っていまいしたが、客観的に暑そうです。 コンビニ行くだけでも汗だくになるので一回お店行った時に買い溜めするようになりました。
本当に、熱中症には十分お気をつけください。
前回の記事に続き、Treasure2020の3日目に学んだことと感想をテキストに起こします。
そして新たなプロトコルへ…
3日目も2日目と同じくバックエンドの講義でした。 ただし、今回はHTTPメインではなく、WebSocketとWebRTCがメインの内容でした。 WebRTCは名前しか知らない状態から始まり、接続プロトコルを学んでちょっと実装をするとこまでしたので自分にかなり多くの学びを得ることができました。
講義で学んだことは以下の通りです。
- WebSocketの概要
- HTTPとWebSocketの違い
- スケーリングの話
- WebRTCの概要
WebSocketの概要
Webベースでオンラインゲーム作ろうとしたことがある方なら必ず一度は調べるであろうWebSocket。 僕も高校の文化祭で展示するオンラインゲームを作る時にWebSocketを使いました。
WebSocketは、低コストで双方向のリアルタイム通信が行える素敵なプロトコルです。 WebSocketという名前にもあるように、実態はただのTCPを用いたソケット通信です(それはそう)。 TCPのコネクションを張って通信路上で任意の通信が行えるので何でも作れます。 例えば、一般的なチャットからマルチプレイ対応の格ゲーまで様々な用途が存在します。
HTTPとWebSocketの違い
新たに登場したWebSocketはHTTPとどのように違うのでしょうか。 細かい違いを挙げていくと無限にありますが、大まかに分けると以下の2つが決定的な違いだと思います。
- ステートレス(HTTP)とステートフル(WebSocket)
- Pull(HTTP)とPush(WebSocket)
ステートレス(HTTP)とステートフル(WebSocket)
ステートレスとステートフル、名前からステートがあるかないかといった違いがあるということはわかるのですが、ステートって一体なんでしょうか。
自分は、サーバに保存されるクライアントとのセッション情報のようなものだと解釈しています。 もっとしっかりした定義や良い解釈があれば教えて頂けると嬉しいです。
ステートレス(HTTP)
HTTPでは、基本的にクライアントから1回リクエストを投げるとサーバがそのリクエストに対するレスポンスを返して1つの処理が完了します。 この手順には、クライアントとのセッション情報のようなものを利用しないため、ステートレスであると言えます。
ステートフル(WebSocket)
WebSocketでは、コネクションを確立した後、そのコネクションを使いまわし、クライアント毎にデータを保存しておくため、ステートフルであると言えます。
Pull(HTTP)とPush(WebSocket)
PullとPushって方向があると思うんですが、これってサーバ側から見た場合なのかクライアント側から見た場合なのかどちらなんでしょう。
これは、サーバ側から見た場合になります。
Pull(HTTP)
Pullは、クライアントからサーバにリクエストを送ってそのレスポンスを返すような挙動を指します。 HTTPは、この方法で通信を行うため、Pull型であると言えます。 クライアント駆動(クライアント側でのイベント発火がメッセージングのトリガになる)と考えることができます。
Push(WebSocket)
Pushは、サーバからクライアントにメッセージを送る(クライアントからのリクエストはいらない)ような挙動を指します。 WebSocketは、この方法で通信を行うこともできるため、Pushが可能です。 ただし、WebSocketは双方向の通信が可能なため、もちろんPullのような動作も可能です。 これによって、基本的にHTTPではクライアント駆動しかできませんでしたが、WebSocketではサーバ駆動(サーバ側でのイベント発火がメッセージングのトリガになる)が可能になったと考えることができます。
ユースケース
始めに自分の結論を述べると、ステートを持つ必要があったりリアルタイム性がほしい場合はWebSocketを利用し、それ以外の場合は実装のしやすさからHTTPを用いれば良いと思います。
HTTP
ステートレスという性質から冪等性があるため、コンテンツのキャッシュを行うのに向いている気がします。 また、コネクションを維持する必要がないため、リアルタイム通信を行わないのであれば無駄なくサーバとのやり取りを行うことができることや、ステートを管理する必要がないため、ステートフルなものと比べてロジックが簡単になります。
WebSocket
WebSocketの概要でも述べた通り、リアルタイム性の必要なサービスに向いています。 また、Webアプリのクライアント間の同期の面からサーバ駆動にしたい場合は多く、そのような場合にはWebSocketが向いていると思います。 例えば、既に挙げたオンラインゲームやチャットがその典型例です。
スケーリングの話
WebSocketはサーバがクライアント毎にステートを管理して通信を行うため、負荷分散等でスケーリングをする際にはちょっと一工夫必要になります。
例えば、WebSocketを用いて複数人でチャットができるサービスがあったとします。
元々、サーバが1台(S1)だけであったものを2台(S1とS2)に増やすことを考えます。
この場合、各クライアントがS1またはS2のどちらかに接続することになりますが、S1はS2に接続しているクライアントの情報を知らず、S2はS1に接続しているクライアントの情報を知らないといった状態になります。 この状態では、各クライアントのチャットのメッセージを全クライアントに共有することができません。
これを解決するには、S1とS2の間にもコネクションを張ります。 そして、S1が知らないクライアント宛への通信はS2に委任し、S2が知らないクライアント宛への通信はS1に委任します。 委任する際には、S1とS2の間に張ったコネクションを通して情報を交換すれば良いです。
このようにして、WebSocketを利用するサーバのスケーリングを行うことができます。
WebRTC
ちょっとここを詳しく書く時間が無いため、参考にしたページを示します。 以下のMDNのドキュメントに全てが書かれています。
講義では、主にWebRTCの接続プロトコルについて学習しました。 接続プロトコルは、以下のドキュメントで確認することができます。
さいごに
これまで全く触ったことのない技術(WebRTC)に触れてめちゃくちゃ興奮してました。 WebRTCに関するMDNのドキュメントが綺麗に整備されていたおかげでスムーズに学習することができました。
自分がある程度知っているものについて深く考えて理解を深めるのと自分が全く知らないものについて学習して理解するのでは、頭の使い方が全然違うと感じます。 これまでの2日間が前者だったため、後者に該当する今回の内容は良い刺激になりました。
Treasure2020 2日目
はじめに
こんにちは。トコロテンです。
空、青いですね。雲、白いです。僕のMacBook Proより白くてびっくりしました。
最近、夏の空が綺麗なので外に出たときは是非一度見上げることをおすすめします。 ただ、熱中症には十分お気をつけください。
前回の記事に続き、Treasure2020の2日目に学んだことと感想をテキストに起こします。
Let's Go!
2日目は、バックエンドの講義ということでGoで書かれた簡単なWebアプリをベースに様々なことを教えていただきました。 Goは、インターンが始まる前にA Tour of Goで予習していたところ、とても気に入りました。 そのため、講義に対するモチベーションがかなり高かったです(実際楽しかった)。
講義で学んだことは、以下の通りです。
- テスト駆動開発(TDD)
- テーブル駆動テスト(TDT)
- Health check
- REST APIにおけるエンドポイントの設計
- RESTfulのレベルの概念
- HTTPステータスコードの選び方
- サーバーサイドのエラーの詳細の公開範囲
テスト駆動開発(TDD)
以下のような手順で行う開発手法をテスト駆動開発(Test driven development)と呼ぶのだそうです。
- テストを書く
- テストをPASSするような実装を行う
- リファクタリングする
これ、初めは「要はテストを先に書くってことね!」くらいしか思ってなかったんですが、よく考えてみるとトップダウンの考え方に沿っているのかなと思いました。 上の手順は、以下のように言い換えることができます。
- こういう風に動いてほしい!(テスト)
- そう動かすためにはこうやって…(テストをPASSするような実装を行う)
- 細かいとこの修正(リファクタリングする)
開発者の「こんな感じに動くやつが欲しい!」といった抽象的なアイデアからその抽象度を下げてより具体的な実装へと落とし込んでいく流れがトップダウンになっていると感じました。
実は、これまでGoでテストコードを書いたことが無く、Treasureで初めてGoでテストコードを書きました。
以下のUnique関数を対象にして、ユニットテスト(単体テスト)を行いました。
Unique関数(unique.go)
package etc func Unique(elems []string) []string { result := make([]string, 0) exists := map[string]bool{} for _, elem := range elems { if _, ok := exists[elem]; !ok { result = append(result, elem) exists[elem] = true } } return result }
ユニーク関数は、文字列のリストを受け取って重複を取り除いたリストを返す関数です。
Unique関数のテストコード(unique_test.go)
package etc import ( "reflect" "testing" ) func TestUnique(t *testing.T) { elems := []string{"go", "js", "go"} if want, got := []string{"go", "js"}, Unique(elems); !reflect.DeepEqual(want, got) { t.Fatalf("Want %v but got %v", want, got) } }
テストケースが1つしかないですが、とりあえずテスト入門なので目を瞑ります。
Goにおけるテストコードのファイル名は、テスト対象のコードのファイル名に_test.go
を付与するという命名規則があるそうです。
Goに入ってはGoに従え
といった有名な言葉もあるのでこれに従います。
テストしてみよう!
カレントディレクトリにあるテストを走らせたい時には、以下のコマンドを実行します。
コマンドを実行すると、ファイル名の末尾が_test.go
となっているテストコードが実行されます。
go test -v .
テーブル駆動テスト(TDT)
Goでテストができた!めでたしめでたし。といきたいところですが、上のテストは改善の余地があります。 改善すべき点は、以下の通りです。
- テストケースの網羅性が低すぎる(複数個欲しい)
- テストケースが何を保証するためのケースなのか分かりづらい
- 入力と期待する出力はケースごとに対であるにも関わらず、データの在り処が分散している
これらを解決できるテスト手法として、テーブル駆動テスト(Table driven test)なるものがあるようです。 テーブル駆動テストを利用すれば、テストケース毎に入力と期待する出力を1つのデータ構造として表現し、そのリストを用意することで複数のテストケースを綺麗にまとめることができます。
上で示したテストケースを、テーブル駆動テストの方式に変更してみると以下のようになります。
package etc import ( "fmt" "reflect" "testing" ) type TestCase struct { in []string out []string desc string } func TestUnique(t *testing.T) { testcases := []TestCase{ { in: []string{"go", "go", "js"}, out: []string{"go", "js"}, desc: "First unique", }, { in: []string{"go", "js", "js"}, out: []string{"go", "js"}, desc: "Last unique", }, { in: []string{"go", "js", "go"}, out: []string{"go", "js"}, desc: "Middle unique", }, { in: []string{"", "js"}, out: []string{"", "js"}, desc: "Empty string", }, { in: []string{}, out: []string{}, desc: "Empty list", }, } for _, testcase := range testcases { t.Run(fmt.Sprintf("%v", testcase.in), func(t *testing.T) { if want, got := testcase.out, Unique(testcase.in); !reflect.DeepEqual(want, got) { t.Fatalf("Want %v but got %v, [%s]", want, got, testcase.desc) } }) } }
上記の変更によって、以下の恩恵が受けられます。
- 複数のテストケースに対応
desc
プロパティでテストケースが何を保証するのかわかる- 入力と期待する出力が1つの構造体の中にまとまっている
美しいですね。これからテストはテーブル駆動テストを使うことにします。
Health check
みなさん、健康ですか。僕は健康です。
ラーメンばっか食べてちゃだめですよ。僕は食べますが。
それはさておき、題名のHealth checkってなんでしょうか。 ここでのHealth checkとは、サーバが正常に動作しているか確認することを指します。
サーバが生きているか確認するための方法として、ping
コマンドなんかがあると思います。
サーバがネットワークに正常に接続しているか、または正常に動作しているかといったことを確認する時に使いますよね。
講義では、もうちょっとレベルの高いHealth checkについて学びました。
ping
コマンドでは、コンピュータ自身が正常に動作しているか確認することができても、特定のサービスやコンテナが動作しているかを確認することはできません(僕が無知なだけでもしかしたらできるかもしれません)。
そこで、より詳細な情報を得られるようにするために、Health check用のエンドポイントを作成します。
以下は、Web APIサーバが利用するDBが生きているか確認するためのエンドポイントです。
// Health check endpoint r.Methods(http.MethodGet).Path("/ping").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // DBの生存確認 if s.db == nil { w.WriteHeader(500) } else { err := s.db.Ping() if err != nil { w.WriteHeader(500) } else { w.WriteHeader(200) } } })
以上のエンドポイントを作成することで/ping
のエンドポイントにGETリクエストを送るとレスポンスのステータスコードからDBが正常に動作しているかどうか分かります。
一般的なGETメソッドを利用しているため、エンドポイントのURLに確認したい項目のパラメータを指定してカスタマイズすることも可能になります。
JSONで詳細データをレスポンスとして返すようにして、定期的にエンドポイントを叩いてJSONをパースすれば、解像度の高いHealth checkが行えそうです。
REST APIにおけるエンドポイントの設計
みんな大好きREST APIを設計する時に直面する問題について議論しました。
主に以下のことについて議論しました。
エンドポイントとリクエストのどちらに資源の情報を持たせるのか
以下のような部屋情報を表すRoom
といったリソースが存在します。
Roomリソースの定義
type Room struct { ID int64 `db:"id" json:"id"` Title string `db:"title" json:"title"` }
Roomリソースに対応するエンドポイント
/room
現時点では、誰でもRoom
資源にアクセスできますが、これに公開・非公開といった概念を加えたいとなったとします。
ここで、部屋が非公開かどうかといった情報をどこに持たせるかといった問題が発生します。
新規モデルを作成
1つのアイデアとしては、以下のような非公開の部屋を表現するモデルを新たに作成し、エンドポイントも専用のものを用意します。
type PrivateRoom struct { ID int64 `db:"id" json:"id"` Title string `db:"title" json:"title"` }
/private_room
既存モデルを拡張
もう1つのアイデアとしては、以下のようにRoom
モデルのアトリビュートに非公開情報を追加し、リクエスト時にこのアトリビュートに値を設定してPOSTしたり、GETパラメータで指定したりします。
type Room struct { ID int64 `db:"id" json:"id"` Title string `db:"title" json:"title"` IsPrivate bool `db:"is_private" json:"is_private"` }
例えば、非公開の部屋を取得したい時には、以下のようなエンドポイントに対してGETリクエストを送ります。
/room?is_private=1
どっちがいいの
上で述べた2つのアイデアは、一長一短です。 具体的には、以下のようなメリット・デメリットがあります。
個人的には、既存モデルを拡張する方がやりやすいかなと思います。 なぜなら、エンドポイントが増えるとどこのエンドポイントにリクエストを飛ばせばいいか分からなくなりそうだからです。
新規モデルを作成
メリット
エンドポイントを別々に分けることができるため、1つのエンドポイントの処理がスリムになる。
デメリット
モデルのアトリビュートが増えるにつれて、エンドポイントが増えていく。
既存モデルを拡張
メリット
既存のエンドポイントをそのまま利用することができる。
デメリット
モデルのアトリビュートが増えるにつれて、1つのエンドポイントの処理が複雑になる。
エンドポイントの命名規則
REST APIのエンドポイントを設計する上で命名規則は大事な要素になります。 命名規則のうち、資源の名前を単数形にするか複数形にするかといった問題があります。
ネットでREST APIについて調べてみると資源の名前は複数形にしている場合が多いように感じました。 調べたわけではありませんが、多くの場合において資源が単一で存在していることはなく、レスポンスもリストを返した方が使い勝手が良いこともあって複数形になっているのではないかと思います。
しかし、上で登場していた/room
エンドポイントは単数形でした。
一見、命名が不適切であるかのように思われますが、あえて単数形を採用するメリットがあります。
それは、資源の名前を複数形にする際に単語の変形を考えなくても良いということです。
たとえば、man
といった単語は、複数形にする場合、語尾にs
を付けるのではなくmen
になります。
こういった単語は、いくつも存在し、それぞれに対応するのは面倒くさく、間違える可能性があります。
また、多くの場合において資源が単一で存在していることが無いのだったらそもそもあえて複数形にする理由も無い気がします。 シンタックスとセマンティクスの癒着を考え、論理的には複数形にすべきだと思いますが、実用的・合理的なのは単数形の方であると思います。
RESTfulのレベルの概念
RESTfulにレベルがあるって知ってましたか。僕は知りませんでした。
厳密には、RESTful成熟度の3レベルモデル(0~3レベル)といったものが存在するようです。 それぞれのレベルは、以下のようになっています。
レベル0
レベル0は、Web APIに1つのURLと1つのHTTPメソッドしか存在しない状態です。 どのAPIが呼ばれたのかは、リクエストボディ等の送信したデータを用いて識別します。
レベル1
レベル1は、レベル0の状態からWeb APIのURLが複数個になった状態です。
レベル2
レベル2は、レベル1の状態からHTTPメソッドが分かれた状態です。 ここがいわゆるRESTfulな状態になります。
レベル3
レベル3は、レベル2の状態からWeb APIのレスポンスにハイパーリンクが含まれる様になった状態です。 例えば、商品を購入するAPIを叩くとレスポンスに返金用のエンドポイントのハイパーリンクが含まれていたりします。 これによって、Web APIの全体像を知っていなくても、エンドポイントから他のエンドポイントへとリンクしていくことが可能になります。
HTTPステータスコードの選び方
HTTPのステータスコードは、大きく分けて以下の5つがあります。
- 1xx系 情報
- 2xx系 成功
- 3xx系 リダイレクション
- 4xx系 クライアントエラー
- 5xx系 サーバエラー
よく利用するのは、2xx系と4xx系と5xx系だと思います。
以前まで不具合や不正があった場合に4xx系と5xx系のどちらを返せばいいか迷うことが多々ありました。 例えば、パスワードが間違っていた場合に4xx系と5xx系のどちらを返せばいいのか迷ったりしました。 しかし、これはステータスコードの範囲の捉え方を変えることで簡単に解決してしまいました。
- 4xx系 クライアントエラー
- 5xx系 サーバエラー
元々は、上記のようになっていましたが、これを以下のように捉え直します。
- 4xx系 クライアント側の責任
- 5xx系 サーバ側の責任
このように「責任」がどこにあるのかといったことに着目すると、パスワードが間違っていた場合はクライアント側の責任であるため、4xx系のステータスコードを返せば良いことがすぐにわかります。
サーバーサイドのエラーの詳細の公開範囲
サーバーサイドのエラー(ステータスコード5xx系)の詳細は、基本的に隠したほうが良いと思います。 講師の方もそのように言っていました。
なぜなら、詳細なエラーをレスポンスとして返してしまうとサービスのネットワーク構成やソフトウェアのバージョン等、脆弱性の発見に繋がる情報が漏れてしまう可能性があるからです。
以下のように5xx系のエラーの場合のみエラーメッセージを空文字列としてステータスコードだけレスポンスとして返すのが個人的なおすすめです。
func RespondErrorJson(w http.ResponseWriter, code int, err error) { log.Printf("code=%d, err=%s", code, err) if e, ok := err.(*HTTPError); ok { RespondJSON(w, code, e) } else if err != nil { he := HTTPError{ Message: err.Error(), } if code >= 500 { he.Message = "" } RespondJSON(w, code, he) } }
さいごに
1日ではテキストに起こしきれない量のことを学びました。 この記事に書いてあることも講義で学んだことと自分が考えたことの一部だけです。 それだけ濃密な時間でした。 本当は全部忘れないように書き残したい気持ちでいっぱいなのですがそれだとブログの記事を書くのが一生終わらなくなりそうなのでこの程度に留めました。
まだインターンに2日間しか参加していないという事実に驚きを感じます。
Treasure2020 1日目
はじめに
こんにちは。トコロテンです。
ついこの間まで梅雨で脳みそ破壊されそうでした(偏頭痛)。 段々と暑くなってきて蝉の声も聞こえるようになると本当に「夏」が始まったんだなぁと実感します。
今年の夏は、VOYAGE GROUPさんのサマーインターンである「Treasure2020」と共に始まりました。 今回は、Treasure2020の初日に学んだことと感想をテキストに起こしたいと思います。
↑これかっこよくないですか。僕はかっこいいと思います。
JavaScript
みなさん、JavaScript書けますか?僕は書けません。
😎「でも、書けるようになるから大丈夫。そう、Treasureならね。」
初日は、フロントエンドの講義ということでJavaScriptの歴史やTipsを教えてもらいました。 挙動不審な言語として悪名高いJavaScriptですが、本講義を通して理解が深まりました。
講義の主な内容としては、以下のとおりです。
- プリミティブ型とオブジェクト型
- イベントループ(event loop)
- function式とアロー関数式
- Promiseオブジェクトとasync/await構文
プリミティブ型とオブジェクト型
プリミティブ型
プリミティブ型に属するデータ型は、以下の6種類です。
- 文字列
- 数値
- BigInt
- 真偽値
- undefined
- symbol
プリミティブ型は、以下のような特徴を持ちます。
- オブジェクトではない
- メソッドを持ない
- イミュータブル(変更不可)である
しかし、メソッドを持たないといった特徴がありながら以下のコードが動作します。
const hoge = "hoge"; console.log(hoge.toUpperCase());
hoge
は、文字列なのでプリミティブ型になりますが、toUpperCase()
メソッドを呼び出すことができます。
これは、各プリミティブ型に対応するラッパーオブジェクトといったものが存在し、自動的にオブジェクトに変換が行われているからだそうです(初めて知った)。
例えば、文字列の場合、以下のようにString
(string
ではないことに注意)に変換されて、メソッドの呼び出しが行われます。
const hoge = new String("hoge"); console.log(hoge.toUpperCase());
オブジェクト型
オブジェクト型は、任意のプロパティを持たせることができるデータ構造です。
分かりやすい例だと、以下のコードにあるobj
のようなものです。
const obj = { hoge: "hogehoge", fuga: "fugafuga", };
参照渡しと値渡し
プリミティブ型とオブジェクト型では、変数のコピーを行う際の挙動が異なります。 具体的には、以下のようになります。
- プリミティブ型: 値渡し
- オブジェクト型: 参照渡し
プリミティブ型(値渡し)
const hoge = "hoge"; let fuga = hoge; fuga = "fuga"; console.log(hoge === fuga); // => false console.log(hoge); // => "hoge" console.log(fuga); // => "fuga"
オブジェクト型(参照渡し)
const hoge = { data: "hoge" }; let fuga = hoge; fuga.data = "fuga"; console.log(hoge === fuga); // => true console.log(hoge.data); // => "fuga" console.log(fuga.data); // => "fuga"
上のコードから分かるように、オブジェクト型は、オブジェクトへの参照をコピーしているだけなので同一オブジェクトのプロパティを変更したりすることができます。 特に、関数にオブジェクト型の引数を渡すときには、内部でプロパティが変更されないかしっかりと考える必要があります。
イベントループ
JavaScriptは、DOMの更新や関数の実行等、様々なタスクを処理しますが、これらは全てシングルスレッドで行われているそうです。 つまり、並行処理はできても並列処理はできないということになります。 したがって、重い処理が走ると全体の処理をブロックしてしまいます。
例えば、以下のコードで実際にcounter
DOMの値が更新されるのは、times
の値が1000になってからです。
const button = document.getElementById('heavyCount'); const counter = document.getElementById('counter'); button.addEventListener('click', function() { let count = 0; let times = 1000; function loop() { if (count++ < times) { counter.innerHTML = count; loop(); } else { alert("Done"); } } loop(); });
このようになる理由は、loop()
関数が実行されるとシングルスレッドであるJavaScriptは、loop()
関数が終了するまでDOMの更新タスクを処理することができないためです。
これを回避するためには、以下のようにsetTimeout
等の関数を利用して非同期処理を行います。
タスクの一部が終わるごとにスレッドを明け渡し、次のタスクをキューに積むといったことをしてDOMの更新タスクが割り込めるようにします。
const button = document.getElementById('heavyCount'); const counter = document.getElementById('counter'); button.addEventListener('click', function() { let count = 0; let times = 1000; function loop() { if (count++ < times) { counter.innerHTML = count; setTimeout(loop); } else { alert("Done"); } } loop(); });
function式とアロー関数式
JavaScriptの関数の定義方法は、以下のようにfunction式を使う方法とアロー関数式を使う方法の2種類があります。
これら2つの方法は、若干異なる点が存在します。
その中でも、特に重要なthis
の束縛のタイミングの違いについて学びました。
- function式: 実行時にthisを束縛
- アロー関数式: 定義したときにthisを束縛
以下のコードを見ると関数の定義方法におけるthis
の束縛タイミングの違いが分かりやすいと思います。
function regular() { return this; } console.log(regular() === window); // true const arrow = () => { return this; } console.log(arrow() === window); // true const obj = { regular: regular, arrow: arrow } console.log(obj.regular() === obj); // true console.log(obj.arrow() === obj); // false
Promiseオブジェクトとasync/await構文
以下のように、Promiseオブジェクトとthen(), catch()を利用したコールバック処理からasync/await構文を利用してコールバックのネストを減らすことができます。 個人的な感想として、今あえてPromiseオブジェクトを使う理由というのは殆ど無いのではないかと思います。
const sleep = (msec) => { return new Promise((resolve, reject) => { setTimeout(() => resolve(msec), msec); }) } // Promise const f = (msec) => { sleep(msec).then((res) => { console.log("f: " + res); }); } f(1000); // async/await const g = async (msec) => { const res = await sleep(msec); console.log("g: " + res); } g(1000);
開発パート
講義の後半でSkyWayといったサービスを紹介してもらい、実際にSkyWayのSDKを利用してビデオチャットのWebアプリを開発しました。 はじめは、機能としてビデオ通話しか無いものでしたが、講義が終わった後も開発がとても楽しかったため、テキストチャットの機能や画像の送信機能を付けてみました。 開発するにあたって、React+TypeScript+SkyWay SDKを利用しました。
以下の画像が、作成したビデオチャットのスクリーンショットです。
さいごに
まだ1日しかインターンに参加していませんが本当に勉強になって幸せです。 プログラムに関しては、これまで殆ど独学だったため、人に教えてもらうということが新鮮でした。 今回の講義でレベルの高い方に教えてもらえるのはめちゃくちゃ効率的な勉強に繋がると実感しました。
GitHubのリポジトリに初めてプルリクが来た話
こんにちは。トコロテンです。
以前、Nintendo SwitchのJoy-ConをPythonで使えるようにドライバを書きました。 これをGitHubで公開していたところ人生で初めてのプルリクを頂いて興奮したという話です。
以下がそのリポジトリです。
初めてのプルリク
経緯
このプルリクを送ってくれた方は、Contributorsにもある通り、atsukobaさんという方です。初めてのプルリクを見ると私が以前書いたQiitaの記事を読んで開発に参加してくださったようです。今では記事を投稿して本当によかったと思います。
プルリクの内容
初めてのプルリクは、私が書いたプログラムのリファクタリング(PEP8対応等)やパッケージングするために必要な変更が主な内容でした。 自分でPythonのプログラムをしっかりとパッケージングしたことがなかったため、コミットの差分を見るだけで面白かったです。 どのようにすればパッケージングができるのかなんとなくわかり、その後は自分で調べたりもしました。
コードレビュー
プルリクが来るとコミットの差分を閲覧しながら、コードレビューを行うことができるようになります。 コードレビューの画面では、コードの行単位でコメントをつけることができます。 以下は、コードレビュー の画面で実際に私がつけたコメントです。
おそらく、setup.pyのテンプレートなるものがあり、その情報を編集せずにコミットしたのだと思われます。(この記事が元だと思われる)
初めてのコードレビューでコードに間違いがあってかつそれを指摘することができたのは運が良かったと思います。
コードレビューで指摘した点を直してもらい、その後マージするとcontributorsにatsukobaさんが追加されました。こうして私の初めてのプルリク対応が無事終わりました。
嬉しかったこと
まず、自分が作ったプログラムに他人が興味を持ってくれて、実際に開発に参加してくれるといったことが堪らなく嬉しかったです。
そして、自分の作ったプログラムが勝手に進化していくという点が見ていて本当に楽しいです。 初めてのプルリクの後にもさらにプルリクが来ました。 そこでは、READMEも書いていただいた上になんとこのパッケージのロゴを作って提供してくださいました。 このロゴを見たときには本当にかっこよくて興奮しました。 以下がそのロゴ画像です。
また、パッケージをPyPIで公開しようといった旨のissueも立ち、実際に登録しました。 以下がプロジェクトのページです。
今では、pip install joycon-python
とタイプするだけで私たちが作成したパッケージをインストールすることができます。
ここまで来ると私も少し世界に貢献できたのではないかと思えるようになりました。 自分のために作って自分しか使っていなかったものが認められて誰かのために役立つ、こんなに嬉しいことは無いと思います。 こんなにもワクワクしたのは久しぶりでした。 最近はしっかりとしたGitの使い方を学んだり、OSSに興味を持ったりするようになりました。
AtCoder Beginner Contest 126 D - Even Relation
問題
考察と解法
世の中には2種類の頂点が存在する。白色の頂点と黒色の頂点である。
by トコロテン
はい、そうですかといった感じですね。
さて、今回の問題を整理すると以下のようになります。
- N個の頂点を持つ重み付きの木構造のデータが入力として与えられる。
- 同色の任意の2頂点の距離が偶数であるという条件の下、各頂点に白か黒の色を塗る。
2頂点の距離というのは、ある頂点から別の頂点へ移動する際に通る辺の重みの総和の最小値になります。
入力とそれに対応する解のイメージとしては以下の図のような感じです。ある解の各頂点の色を反転させたものもまた解となります。
例を確認したところで考察をしていきます。 考察するにあたって、以下の2つの関数を定義します。 $$ dist(a, b) := 頂点a, b間の距離 \\ color(v) = \left\{ \begin{array}{} 0 & (頂点vが白) \\ 1 & (頂点vが黒) \end{array} \right. $$
まずはじめに、任意の頂点Sを1つ選び、頂点sに色を塗ってみます。 この際、塗る色は白と黒のどちらでもいいはずです。 なぜならば、上でも説明した通り、ある解の各頂点の色を反転させたものもまた解となるため、ある頂点が白なのか黒なのかは本質ではなく、どの頂点とどの頂点が同じ色なのかということのみが重要だからです。
解の条件 $$ color(a) = color(b) \Rightarrow dist(a, b) \equiv 0 \bmod 2 $$
の対偶をとると以下のようになります。 $$ dist(a, b) \equiv 1\bmod 2 \Rightarrow color(a) \neq color(b) $$
したがって、の色がに決定します。
では、反対にとした時にについてとしてよいのかといった疑問が残ります。 これは結論からするとしてよいです。
以下を示します。 $$ dist(e_1, e_2) \equiv 0\bmod 2 (\forall e_1, e_2 \in E) $$
まず、sを木の根とし、とを達成する経路上でから根の方向に向かって初めに合流する点をとします。
この時、 $$ dist(e_1, e_2) = dist(e_1, s) + dist(e_2, s) - 2 * dist(v_p, s) \\ $$
となります。また、 $$ dist(e_1, s) \equiv 0\bmod 2 \\ dist(e_2, s) \equiv 0\bmod 2 \\ 2 * dist(v_p, s) \equiv 0\bmod 2 $$
であるから、 $$ dist(e_1, s) + dist(e_2, s) - 2 * dist(v_p, s) \equiv 0 \bmod 2 $$ が成立します。
したがって、 についてとしてよいです。 これによって、頂点sを1つ選ぶことで残りの全ての頂点の色が頂点sとの距離の偶奇によって決定できることがわかりました。 特に、とすることで任意の頂点vについて以下が成立します。 $$ color(v) = dist(s, v) \bmod 2 $$
以上を踏まえて、アルゴリズムの計算量を見積もります。
計算量
適当な頂点を1つ選びそこから全ての頂点への距離を求めればよいため、計算量はとなります。
実装上の注意点
木を表現するにあたって、頂点間の辺の接続情報を隣接行列で表現しようとすると各頂点について接続されている遷移先の頂点を調べるのにかかり、全体の計算量がになってしまいます。 したがって、辺の接続情報は工夫して保持しましょう。具体的な実装方法は以下のコードを見てください。
ソースコード
#include <iostream> #include <vector> using namespace std; void set_color(int node, int color, vector<vector<pair<int, int> > >& edges, vector<bool>& hasVisited, vector<int>& colors) { colors[node] = color; hasVisited[node] = true; for(auto edge : edges[node]) { int next_node = edge.first; int cost = edge.second; if(!hasVisited[next_node]) { set_color(next_node, (color + cost) % 2, edges, hasVisited, colors); } } } int main() { int n; vector<vector<pair<int, int> > > edges; vector<bool> hasVisited; vector<int> colors; cin >> n; edges.resize(n + 1); hasVisited.resize(n + 1, false); colors.resize(n + 1); for(int i = 0; i < n - 1; ++i) { int u, v, w; cin >> u >> v >> w; edges[u].push_back(make_pair(v, w)); edges[v].push_back(make_pair(u, w)); } set_color(1, 0, edges, hasVisited, colors); for(int i = 1; i <= n; ++i) { cout << colors[i] << endl; } return 0; }
感想
木の気持ちになることが大切な気がします。木だけにってね🤗
大学祭でゲームを作った話 - 2019
今年の大学祭について
こんにちは。トコロテンです。私が所属している大学の組織では、毎年大学祭で自分たちが作った作品を展示します。展示物は自作ゲームが殆どです。今年は、以下のような展示物がありました。
- シューティングVRゲーム
- 剣と魔法のVRゲーム
- 声でキャラクターを操作するマリオライクなゲーム
- 手の動きだけで3Dモデルに色塗りができるアプリ
- アスキーアートでSTAR WARSを表現した映像
- ラズパイで取得した温度・気圧・湿度・カメラ映像を閲覧できるWeb
- ジョイコンで遊べる対戦ゲーム
- 体を動かす3Dリズムゲーム
私の作品は、ジョイコンで遊べる対戦ゲームです。
簡単な紹介とダウンロードリンクを以下に置いておきます。ただし、注意点として遊ぶ際にはゲームを2つの端末で同じアスペクト比の解像度で起動してください。異なるアスペクト比の解像度ではおじさんが正常に相手の画面にスポーンすることができません。
大学祭用にジョイコンを使って二人で遊べるゲームを作った!! pic.twitter.com/3ZOF1NmCRN
— トコロテン (@tokoroten_lab) November 8, 2019
実機テストで実際に遊んでもらった際の動画。 pic.twitter.com/rT2LOey0n4
— トコロテン (@tokoroten_lab) November 10, 2019
ゲームの説明
ゲームのタイトルは「たんさんおじさん(SparklinGrandpa)」というタイトルで簡単に説明するとジョイコンを振りまくって画面越しの敵を狙って攻撃するゲームです。
ゲームの操作にはNintendo SwitchのJoy-Conを利用します。
実際のプレイ動画等は上のツイートを見てください。
ジョイコンを降ると「たんさんエネルギー(SparklingEnergy)」が溜まり、これがおじさんを飛ばす力に変換されます。たんさんエネルギーが溜まっている方が飛距離を稼ぐことができ、一度おじさんを発射するごとにたんさんエネルギーが半減します。弾丸になるおじさんには何種類かタイプがあり、見た目と当たり判定の大きさのみ異なります。
各画面には、ヒットボックスとなる「ヒットボックスおじさん(HitBoxinGrandpa)」が存在し、そこに相手が発射したおじさんが当たるとダメージを受けてHPが減少します。
2つの画面の境界は、ネットワーク通信によって仮想的に接続されており、画面端まで飛んでいったおじさんは画面の境界を超えて相手の画面におじさんタイプ・速度・加速度・回転角を引き継いだ状態で登場します。
ゲームの設計時に考えたこと
ターゲット層
このゲームを設計する際に最も重要視したのは遊んでくれるターゲット層です。去年の大学祭の来場者の傾向から、圧倒的に親子の割合が多く、小学生くらいの子が多かった記憶がありました。したがって、ターゲット層はこの方々に設定して設計しました。私は幸いなことに小学生の弟がいるため、弟を仮想客として考えました。
まず、最近の小学生は、スマホ世代であることからパソコンを使う機会が少なく、キーボードによるゲーム操作は不適であると考えました。小さい子に身近で直感的かつPCで利用できるインターフェースを提供する必要があり、これを満たすのがSwitchのJoy-Conであると考えました。私が小学生の頃はNintendo DSやWiiが流行のゲームデバイスでしたが、現在は小学生がよく遊ぶゲーム機といえばNintendo Switchです。このコントローラーは彼(女)らからすれば身近であり、操作も直感的かつPCで利用することもできます。これらのことからジョイコンをゲームのコントローラに採用しました。また、小さい子は頭を使うよりも体を動かした方が楽しいだろうと考え、ジョイコンを振るといった動作をゲーム内に取り入れました。
そして、親子連れが多いといったことから、親御さんと一緒に小さい子が遊べるゲームが良いと考えました。また、リアルタイム性のある対戦ゲームにすることでプレイを見ている方も応援できるようになるのではないかといった考えもあり、今回のようなゲームを設計することになりました。
余談ですが、小さい子どもは、本当に正直に思ったことや感想を言ってくれます。少し年がある子は気を使ってくれて、楽しくなくても「楽しい」と言ってくれますが、年齢の低い小さい子どもからは、「なにこれ!意味分かんない!」といったような正直なフィードバックを得ることができます(実際に頂いた意見です)。この貴重な意見を利用しない手はありません。小さい子の指摘に耳を傾け、小さい子でも難なくプレイできる操作性やゲームの設定を維持した上で楽しいゲームを提供することは、一種のユニバーサルデザインを実現することにつながると考えています。
フィードバック
このゲームを設計する際に次に重要視したのはプレイヤーのアクションに対するフィードバックです。
まず、おじさん発射時のたんさんエネルギーの半減によってプレイヤーはおじさんを発射する度にしっかりとジョイコンを振らないと発射されたおじさんの飛距離が短くて相手の画面まで届かない設計になっています。したがって、プレイヤーは勝つために必ずジョイコンをしっかりと振る必要があります。一度プレイしてみるとわかりますが、この「ジョイコンを振る」といった動作は思っているよりも腕が疲れます(そのように設計しました)。言い換えるならば、コストが掛かります。ジョイコンを振るといったコストの掛かる動作に対して、おじさんの飛距離が伸びるといった一種の報酬が存在します。これがまず第一のフィードバックです。
次に、相手のおじさんが自分のヒットボックスおじさんに当たった際には、以下の現象が発生するようにしました。
- 画面全体が一瞬赤くなる
- ジョイコンが振動する
- ヒットボックスおじさんが回転する
- ヒットボックスおじさんが痛そうな声を出す
- HPゲージが減少する
ダメージを受けたということに対する危機感を与えるために、視覚・触覚・聴覚に訴えるようなフィードバックを用意しました。人間の五感は視覚に大きく依存していると言われています。したがって本来なら画面全体が一瞬赤くなるといった現象はプレイヤーに大変わかりやすく情報を伝達することができますが、このゲームの場合、それだけでは足りません。なぜならば、「おじさんを相手の画面のヒットボックスおじさんに当てる」といった基本アクションを行う際には相手の画面をしっかりと見る必要があり、自分の画面が赤くなっていることはわからないためです。したがって、自分の画面を見ていないときでもプレイヤーにダメージを受けたというフィードバックを与える必要があります。これを「ジョイコンが振動する」「ヒットボックスおじさんが痛そうな声を出す」といった触覚・聴覚に訴えるフィードバックを与えることで解決します。HPゲージが必要なのは言うまでもありません。
開発に関する技術的な話
ゲームエンジン
ゲームを作るにあたって、Unityを利用しました。開発言語はC#です。
通信関係
このゲームには以下の3種類の通信が存在しています。
PCとJoy-Con間のBluetooth(HID)通信については、以下のリポジトリのドライバを利用しました。かなり良くできており、サンプルも付属しているためスムーズに導入できました。
TCPは対戦相手との接続に利用しています。対戦相手との接続用の通信には信頼性が必要であったためTCPを利用しました。
実装の際には、.NET FrameworkのSystem.Net.Sockets.TcpListenerとSystem.Net.Sockets.TcpClientクラスを利用しました。
UDPは発射されたおじさんを相手の画面に転送する際に利用しています。発射されたおじさんが画面の境界に行くと送信側は相手側におじさんの情報をまとめたデータグラムを送信しておじさんを削除した後、相手側はそれを受信しておじさんを生成します。これによって仮想的に画面を接続しています。ただし、UDPは通信の品質を保証しないため、送信したおじさんのデータグラムが削除されて相手におじさんが届かない場合があります。これを私は「おじさんのロスト」と呼んでいます。この送信方法の場合、おじさん送信用のパケットはあまり飛び交わないため、負荷も高くなく、一般的にはTCPにしたほうが良いかと思われます。あえてUDPを採用した理由はロマンと実装のお手軽さです。まず、TCPがストリームを成して送受信するデータ境界が存在しないのに対し、UDPはデータグラムといった意味のあるまとまりのデータ単位で送受信し、データ境界が存在します。つまり、おじさんそのものがネットワーク上を飛んでいるのです。ここにロマンを感じます。また、データ境界があることに寄ってプログラムの実装が簡単になります。
実装の際には、.NET FrameworkのSystem.Net.Sockets.UdpClientクラスを利用しました。Unityのスレッドをブロックしないように気をつけて非同期でデータの送受信を行っています。
当日の様子
当日は、去年の大学祭よりも多くの方々がブースに来てくださり、盛況していました。来場者は、予想通り親子の割合が多く、小学生くらいの子が多かったです。したがってターゲット層にバッチリとマッチしており、狙い通りであったと言えます。私が作ったゲームも多くの方に遊んでいただき、お父さんと小学生くらいの息子さんが楽しそうに対戦している姿を見て本当に嬉しかったです。
また、ジョイコンを見て「スイッチ!?」「ジョイコンだ!!!」といった反応をしてくれる子も何人かいてここも狙い通りであったと言えます。中には、ゲームを自分で作ったことのある小学生の子からジョイコンとどうやってPCを接続しているのか聞かれたり、エンジニアの方から「ジョイコンの通信プロトコルは公開されているのか?」といった質問を頂いたりと技術的なお話もできて大変楽しかったです。
自分が作ったゲームを楽しそうに遊んでもらうのは本当に本当に嬉しいです。
反省点
連続で何回かゲームをプレイしているとジョイコンの接続が怪しくなるといった現象が多発しました。その度にゲームを再起動することになり、一部の来場者の方々には少し不快な気持ちをさせてしまったかもしれません。動作を注意深く観察しているとおそらくドライバなどの問題ではなく、私のプログラムのロジックの問題であるのではないかといった予想がつきました。本番前に予め実機テストを十分にしていればバグに気づいてロジックを修正することができたかもしれなかったため、実機テストをもう少し行っていればよかったと後悔しました。