Peruki's Blog
技術や生活に関する記事を載せています
▲ p2hacks会場の様子
ぺるきです。
先日、学内ハッカソンp2hacksに参加し、「この世界は熱すぎる!!!」という作品を開発しました。 結果、学部1・2年の参加枠となるPre-PBL部門にて、優秀賞をいただきました。一緒に開発してくれた2人には本当に感謝しています。そしてp2hacksを支えた運営の先輩方や関係者にも、このような刺激的な場を用意して頂いたこと改めて感謝申し上げます。
この世界は熱すぎる!!!
アンチひんやりを破壊し、世界平和を取り戻せ!!!
リンク: https://p2.jugesuke.net/
(GitHub: https://github.com/p2hacks2023/pre-06)
スマホ縦持ち推奨です。タブレットや横持ちでも一応プレイできます。
PC非対応 (Responsive Design Modeで遊べるのは秘密)。
しばらくプレイできますが、予告なく公開を停止する場合があります。
...要旨だけ説明すると「赤いもの (=アンチひんやり) 」を破壊して気持ちよくなるゲームです。ちなみにハッカソンのテーマは「ひんやり」でした。赤いものは熱い、拡大解釈にも程がある。
普段ゲーム開発をしない3人によるゲーム開発でした。 (特に最初の段階では) うまくいくか終始不安でしたが、なんとか要件のほぼ全てを実装完了し、プロダクトの公開まで漕ぎ着けました。
企画とプレゼンはYourein君が行っています。詳しいところはYourein君の記事が全部語っているのでそちらを見てください。強すぎる世界観もさながら、勢いのあるプレゼンで本当に良かったです。
メインとなるTSでの実装部分は、ぺるきとJugesuke君で開発しました。 今回の開発はぺるきが初期段階で設計・構築した独自フレームワーク上での実装となっており、Jugesuke君にはオレオレ環境を押し付ける形となってしまったので、申し訳ない気持ちです。
またJugesuke君は、CI/CD含むインフラ環境の整備も担当していました。今回のようなゲーム開発においては、実装したグラフィカルなUIや演出が、世界観と合致し、かつ全員が納得する形で実装できているのかをすり合わせる必要がありました。そのため、デプロイ結果を共有するだけで非同期でもレビューを受けられるCI/CD環境は、本当に有り難いものでした。
実装部分にはYourein君も参加しており、ボトルネックになる画像処理の部分をRust (wasm) で実装していました。動作検証の難しい状況で、テストまで書いて出していて助かりました。
ここからは自分が担当した部分の話をします。ソフトウェア設計のほか、ゲームロジックのコア部分、そして各種演出を実装しています。ここではかいつまんで紹介します。
最初の段階で、HTML/Canvas環境上で共同開発するための独自フレームワークを設計・実装しました。ゲームの部品をコンポーネントとして切り離し、それぞれでテストできるようにしています。またブラウザの機能およびフレーム・シーンの管理を抽象化し、イベントとして各コンポーネントで処理できるようにしています。
import Bound from "../geometry/bound";
import Point from "../geometry/point";
import { TouchEndEvent, TouchMoveEvent, TouchStartEvent } from "../model/event";
import { Scene } from "../model/scene";
export interface Component {
// コンポーネントの領域
bound: Bound;
// 画面が描画される時
draw (context: CanvasRenderingContext2D) : void;
// 画面が擦られた時
onScratch? (previousCursor: Point, currentCursor: Point) : void;
// シーンが変化した時 (ただし、コンポーネントが前後両方のシーンで有効である必要がある)
onSceneChanged? (previousScene: Scene, currentScene: Scene) : void;
// タッチされた最初のフレームの時
onTouchStart? (event: TouchStartEvent) : void;
// タッチされている時
onTouchMove? (event: TouchMoveEvent) : void;
// タッチが離された時、またはタッチがコンポーネントから外れた時
onTouchEnd? (event: TouchEndEvent) : void;
}
▲ pre-06/src/engine/component/component.ts
このインターフェイスを実装したクラスを各自で作り、インスタンスを適切に配置すれば、ゲーム上で動かせるようになります。入力イベントを利用する際の面倒な実装 (各コンポーネントの範囲に基づくタッチの判定、使用する座標系の指定、カーソルが外れた時の対応など) も、各コンポーネントを処理する前によしなに処理しています。
半日で開発したので、今改めて見ていると色々とアラはあります。それでも、このフレームワークを原因に技術的課題が発生することは最後まで無かったため、安心しています。
このゲームの中盤で、プレイヤーがスクリーンを擦る動作に合わせて、撮影した画像の熱い (=赤い) 部分をバラバラに崩す演出があります。こちらも僕が実装しました (なお、ピクセルの熱さ判定はYourein君が書いています) 。
崩れる形はボロノイ図に基づいています。 企画段階では、処理の最初に画像をボロノイ分割し、プレイヤーが擦った位置に合わせて各ボロノイセルを落としていく実装を考えていました。一方でこのアプローチは、擦った位置と崩れる図形の位置に無視できない差が生じる可能性があり、操作感に影響が出てしまいます。またこのアプローチには、優先度付きキューやボロノイ分割のようなアルゴリズムの実装が必要です。こういったアルゴリズムは、自分で書くには面倒であるのはもちろん、外部ライブラリに依存してまともに動いた試しがありません。
.... // 擦った点の周囲にランダムに点を作る const voronoiPoints = [[startx, starty]]; let angle = 0.0; // ランダムな点の間が角度が大きすぎると、図形が閉じず破片が無限に広がるので、制限する const minAngle = Math.PI / 3; const maxAngle = Math.PI / 2.2; while (angle < 2 * Math.PI - minAngle) { const random = Math.random () * (maxAngle - minAngle) + minAngle; angle += random; const radius = Math.random () * this.scratchRadius + this.scratchRadius; const x = startx + radius * Math.cos (angle) ; const y = starty + radius * Math.sin (angle) ; voronoiPoints.push ([x, y]) ; } let stack = [[startx, starty]]; while (stack.length > 0) { ... // 隣り合う点を取り込み、ボロノイ図を作る
▲ pre-06/src/engine/component/scratchableImage.ts
そこで、擦った時点でその位置を囲むように4-7個ほどランダムに点をうち、そこで即時にボロノイ図を作成する手法をとっています。擦った位置のピクセルを始点として、隣接するピクセルについて、最も近い点が擦った位置の点でなくなるまで連鎖的に取り込み続けます。これにより、ボロノイセルの外接円の中心が擦った位置とおおむね一致するので、熱い場所がちゃんと擦った場所に合わせて崩れるようになります。加えて、実装の面倒なアルゴリズムを必要としません。
また、崩れ落ちた部分は別のイメージとして書き出して動かすのではなく、画像の打ち込み時に各ピクセルを本来の位置より下に書き出すことで、面倒な実装せずに高速な描画を実現しています。
もう一つの役割として、ロゴをはじめ素材全般を担当しました。僕は中途半端に (いわゆる「当たり前品質」的な水準で) 絵は描けるので 、デザイン学科の人がいない場ではこういった担当に回りがちです。
ロゴはillustratorで作った初めての素材ですが、なかなか気に入っています。
プロダクトを完成させた上、デプロイまで済ませ、発表当日に多くの人にプレイしてもらえたのは本当に良かったです。
グラフィックス的な処理に関するタスクも多く、Webサービスの開発と比べて (個人的には) とても楽しい、というか、生きた心地のするプログラミングができました。演出や素材作成もいろいろ勝手にさせてもらったため、全体的に楽しかったです。
ゲーム開発という慣れない題材もあってか、技術スタックとしてはあまりガチれなかった印象です。Vanilla TS環境の上、描画にCanvasを採用したこともあり、終始「最新の知見を寄り合わせていにしえのWebを書いている」気分でした (それでも、CI/CDを導入したり、Rustを使ってボトルネックを緩和させているのはなかなか変態ではあります) 。
Yourein君も語っていますが 、自分は他のメンバーに対抗して意見を述べることが多かったです。特にリリース準備の段階に関しては、衝突することが多々ありました。発表のスタイルや、ドキュメント (アピールシート) の内容について、意見を投げては「それは今やるべきことじゃない/不要だ」と指摘を受け続けました。
このことについては、どうしても「何にこだわるか (何が不要か) 」で大きな溝が合ったように感じます。 限られた時間しか与えられない以上、不要な作業は避けるべき...という意識は必要です。しかしながら、具体的に何が必要で、何が不要なのか、というところには、どうしても個人差が発生し得ます。
そしてこの溝は、自分とそれ以外のメンバーの間で特に深かったように感じます。自分は見栄え重視なところが大きく、プロダクトそのものよりも、発表やドキュメントへのウェイトが比較的大きかった気がします。そのこともあってか、ところどころ、2人の方針や意見に納得がいかないことがありました。そういった中で、お互いにもやもやしながら、開発を進めていました。
とはいえ、自分が終盤で食い気味になってしまったことは、深く反省しています。 また、Yourein君の発表スタイルしかり、最終的には自分の意見をいくつか汲み取ってくれたこともあり、最後には納得できる結果に収まりました。いろいろと、感謝と申し訳無さを感じます。チーム開発、本当に難しい!
個人的には、えびとシュリンプ (最優秀賞受賞) 、progression、んどんどん、の3チームから強い印象を受けました。おそらくこのいずれかが上位賞を独占するのだろうと思っていたので、うちのチーム (fuNG) が優秀賞を取るのは正直意外でした (それでもやはり企業賞枠は漁られました) 。
特にprogressionには、いろいろと一本取られた気分でした。Three.jsを使ってReact環境上で3Dグラフィックスを実現しており、開発環境・素材・演出含め本当に素晴らしいものでした。
とはいえ率直ながら、Pre-PBL部門の中で、テーマ「ひんやり」に一番合うプロダクトを開発したのは、他でもなくうちのチームだと思います。あくまでも「世の中の課題解決」を求められるハッカソンという場にありながら、本質を放り投げ拡大解釈も辞さない、とち狂った勢いを貫くプロダクトは本当に輝いています。
5000円は何に使おう。「イ良い日ン マ 」でも買おうかしら?