Unity1weekに初挑戦する!制作奮闘記
2024年12月23日(月) 0時 〜 2024年12月29日(日) 20時
との事。初参加だ……。
本当はUnityのチュートリアル完了してから挑みたかったけどシレン6が楽しすぎて全く手についてない。
仕方ないのでぶっつけ本番でやる。ゲームが完成さえしてしまえばこっちのもんだ。
前提条件
・使用バージョンは6000.031f1
・Unity歴は中途半端にチュートリアルをやっては抜けてを繰り返し、個人で1本完成させたことはない
累計で1年ぐらい?
・今まではフリーゲームをいくつかフリーゲームを作ってきました。プログラムの心得はある。
こんな自分ですがUnityにチャレンジしに来たのでUnity1weekに挑戦したいと思います!!イエイ!!!!!!
お題「ない」
2024年12月23日(月) 0時 〜 2024年12月29日(日) 20時
お題が決まった!本格的に開始だ!
お題はちょっと変わった感じの様子。初参加で作るにしてはどうだろうハードル高いのかな。
まずはどんなゲームがいいかをお題から逆算してみる。何にしよう……
プロジェクト作成
大体ゲームの企画が決まったのでまずはプロジェクトを作成してみる。
今回は2Dゲームとなるが、どのテンプレートがいいのだろうか?
どうやら2DはこのUniversal 2Dというものが一般的な模様。
空プロジェクトだが説明文にある「Universal Render Pipeline」とはどういうものなのか?
調べてみるとレンダリングのパイプライン(描画の手続き)の一種で前は色々あったが今は一番いいUniversal Render Pipeline、略してURPが一般的見たい。
なので難しい事考えずこれにする。
出来た出来たと
プロジェクトのリポジトリを作成する
何はともあれまずはバージョン管理!
今回はGitHubとかは使わずローカルでバージョン管理するためだけのリポジトリを作成します。
何でも、Unity用に必要なファイルだけをAddしてくれる設定が既に用意されているのだとか
調べたところによると、Libraryフォルダとかは自動生成されるものなのでバージョン管理が必要なものではないらしいのですのよね~紛らわしい名前だというわけでリポジトリ作成完了。
管理はSourceTreeで行ってます。
まずは必要なアセットを入れる
絶対必要になるものはテキスト系とTween系の2種だと思ってる。
調べてみるとアセットおすすめランキングもあり、その中でも上位に2つとも入っている。何はともあれまずはTextMeshProとDOTweenの2種を設定だ!
フォントは愛用しているものを焼くことにする。
TextMeshProの設定
というわけでフォントをまずは作成する。
キャンバスにTextMeshProを追加するとTMP Importerが出現。
によりますとImport TMP Essentials とはTextMeshProのデフォルト設定ファイルのインストール との事らしい。なるほど?
とりあえずTMP Importerを押下だ!
テキスト作ったよ。ついでにCanvasも作成されるみたい。
ただデフォルトだと日本語は当然使えないみたい。
とりあえずフォントの追加だ!
を選択してフォント作成へ。
Unityのアセットに使用したいフォントをD&Dでインポート。
んで早速インポートしたフォントを選択し、設定通りに従ってGenerate!
……あの……Unity落ちたんすけど……
色々と試したところ、ひらがな/カタカナ/ASCII/JIS第一水準漢字(3353文字)だと落ちる?
ひらがな/カタカナ/ASCII/汎用記号/常用漢字(2524文字)にして試してみる。
それでも落ちる……なんなんお前?
ダメだーファイルから読み込む方式にしても遠慮なく落ちる
???
--- Ten Hours Later... ---
わかったぁ!
ここのPadding設定の欄だ!
Unity6起動直後ですと変なマイナスの値が入って「%」に設定されてましたが
サンプル通り5の「px」を選べば落ちませんでした!なんじゃそりゃ!!
おおおついに出た……!これにてTextMeshProの設定完了です
DOTweenを導入
続いてTween系の大定番!DOTweenを入れます。
その後UnityのPackageManagerを開いて導入!
無事導入が完了したらC#スクリプトを追加してDOTweenの記述を書いてみる
何か右に動いた!これで完成だ!
UniTaskの導入
最後に非同期処理のUniTaskを導入します。この3種の神器が揃えばとりあえず企画に沿ったゲームは作れると思う。
1.パッケージマネージャーを開く
2.左上の「+」マークのボタンを押し、「Git URLからパッケージをインストール」を押下。
URLは下記のを入れる
3.導入完了!
(ちなみにgitを予めPCにインストールして環境変数通してないと、GitURLからはインストールできないです。詳しくはググってね)
最後に確認ついでに簡単なスクリプトを書いてみる。
どうやらDOTweenとの連携も出来るようなのでその連携スクリプトを試してみる。
設定は下記に書いてありました!サンクス!
これで設定完了みたい。
using System.Collections;
using UnityEngine;
using DG.Tweening;
using Cysharp.Threading.Tasks;
public class HelloWorldDOTween : MonoBehaviour
{
async void Start()
{
await transform.DOLocalMove(new Vector3(100f, 0, 0), 1f);
await transform.DOLocalMove(new Vector3(-100f, -100f, 0), 1f);
}
}
コードを試し打ちして……、さっき作ったテキストにコンポーネントを張り付けて……
出来た!!
これで準備が整いました!次からは実際にゲームを作っていくぅ!!
会話画面を作っていく
ストーリーラインから作りたいのと、UI組み立ての練習ついでにやってみる。
次にUI組み立てのために解像度とか先に決めちゃう!
ここを参考に色々と設定。今回は1920×1080で作る事にします。エディター拡張でヒエラルキーでActive切り替えを実装する
でも便利そうなのでこちらを導入していきます。
using UnityEngine;
using UnityEditor;
public static class HierarchyGUI
{
private const int WIDTH = 16;
private const int OFFSET = 40;
[InitializeOnLoadMethod]
private static void Initialize()
{
EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowItemOnGUI;
}
private static void HierarchyWindowItemOnGUI(int instanceID, Rect selectionRect)
{
var go = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
if (go == null)
{
return;
}
var pos = selectionRect;
pos.x = OFFSET;
pos.width = WIDTH;
bool active = GUI.Toggle(pos, go.activeSelf, string.Empty);
if (active == go.activeSelf)
{
return;
}
Undo.RecordObject(go, $"{(active ? "Activate" : "Deactivate")} GameObject '{go.name}'");
go.SetActive(active);
EditorUtility.SetDirty(go);
}
}
上記のコードをAsset/My/Editorフォルダを作成し、HierarchyActiveSwitch.csを作成して導入。
Editorという名前のフォルダ以下であればどこにスクリプトがあってもエディター拡張が効く。
これは忘れがちなので覚えておこう
出来た!
Addressables Asset Systemを導入する
画像を読み込むにはResourcesフォルダに入れておけばスクリプトから動的に読み込める、というのは時代遅れらしいですね。
というわけで今どきはAddressables Asset Systemを使うのがベターみたいですのでこちらも導入していきます!
こちらを参考にやる。
AssetBundleとの関連知識は
こちらから学びながらやる。
まずはパッケージマネージャーからUnityレジストリタブ>Addressablesを選んで導入。
続いて導入したリソースをAddressables Groupsで管理。アドレスを割り振る。
よく分からんけどこんな区分けでいいのかな?まあ良し!!
いよいよ会話画面UIを作る
まずは画像を基にベースとなる配置を行う。
その後にスクリプトで色々と作り込もうと思う。仕様は頭の中にあるがそれを書きだす時間すら惜しいのでそのまま作っていく。
色々やってUIの配置出来た!
イメージとしては、クリックしたら吹き出しを進めていくタイプがいいと思う。
クリックは透明なImageを画面上に設定して、そのOnPointerClickを感知して進めてみる。
どうせ時間がないし経験も薄いので凝ったインプットシステムなんか作れっこねえ!
とりあえずこんな感じに出来た。
クリックでセリフが変わる
ScriptableObjectに会話データベースを設定し、クリックで会話を進める
これもう出来たらゴールでしょ……
続く
会話システムの実装
まずはScriptableObjectで会話データを設計していきます。
以下のようなのを想定。
- テキストデータ
- 画像イメージ
- 同時発生SE
- 吹き出しの種類
- 右側か、左側かのフラグ
- ウェイト
- 再生BGM
クリック毎にこれらのデータを読み込むリスト式にしていく予定。
そうと決まればデータ設定だ!
Scriptable Object の実装
この辺りを参考に実装していくぞー!
……
いろいろやって出来た。そんなわけで会話システムが実装できたわけだ。
主にやったのはScriptableObjectの生成。Addressable Assetに登録。それの動的読み込み。
会話システムはクリックしたら吹き出しオブジェクトを生成していくタイプ。
そんなノリで作成しましたとさ。
いい感じだぁ……
何気にここでお題の「ない」について触れましたね。
地味にはまったポイントですが、DOTweenでImageがぶら下がったGameObjectを動かすとき、
DOLocalMoveY
のようにLocalのtransformを動かす必要がありそうです。DOMoveYですと何か動きが大きく動きすぎて微妙にはまった。
次はオーディオマネージャーを作っていく!
シングルトンで実装して、インスタンスはstatic関数で取得できるようにするつもり
AudioManagerを作成する
続いては音関連を鳴らすために作成するぞ!
まずはシングルトンを実装するベースを作成する。参考にするのはここ
一部非推奨の関数があるので、そこは変えていく。
シングルトンベースが出来たら次はBGMとSEをScriptableObjectに登録。
そして
- Unityにインポート
- Addressable Assetに登録
- Enumで使用するBGM,SEを設定
- ScriptableObjectでEnumから逆引きでアセット名を引っ張れるように実装
- Enum定義されたらアセットを再生させるように実装
という流れでAudioManagerが完成した!
これでBGM再生させつつSEもならせるようになった……NKT……
次はいよいよインゲーム本番作っていく!残り3日!間に合うのか!?!?!?!?!?!?!?!?
シーン切り替え
適切に会話からインゲームに入れるように、シーン切り替えを実装していきます。
ここがスムーズにいかないとゲーム作れるもんも作れない!!
方針
お友達のやり方をまねて、Unity標準のSceneを使うわけではなく、独自のMySceneと名付けたGameObjectを生成して、それをマネージャーで切り替える形で実装してみたいと思います。
全てのシーンをオンメモリに乗せる代わりに切り替えが楽になる手法だー!
SceneBaseクラス作成
まずはこれが必須。必要な機能としては
- シーン開始メソッド
- シーン終了メソッド
かと思います。
シーン開始メソッドはその名の通りシーン実行するための初期化を行うもの。
シーン終了はシーン移動前に呼び出して後片付けを行うもの。
後は初期化用の構造体でも引数に持たせたいですがそのあたりはScriptableObjectのAsset名を渡して対応してみる
SceneManager作成
お次はシーンの切り替えを担うマネージャを作成。こちらもシングルトンで用意。
後はシーン切り替え関数を呼び出された時に、シーン開始メソッドとシーン終了メソッドをそれぞれ呼び出すだけ!
合間にフェードイン/フェードアウトもつけたいね
FadeManager作成
というわけでフェードアウトとフェードインを担うマネージャーも作成!
これで切り替え時にフェードを挟むように出来ました!
切り替え処理実装
後は実装はシンプル。切り替え関数を呼んだら現在のシーンをSetActive(false)して、
次のシーンをSetActive(true)するだけ。
その合間にリソースも解放するようにしております。
試しにタイトルのテストを作って
無事会話シーンに突入。完璧だぁ……
インゲーム作成
いよいよ本番。
インゲームはカードゲーム作ります!
そのためにはまずカードに必要な情報となるScriptableObjectの作成。ずっとこればっかだな……
カード用のScriptableObject作成
今回はカード効果は決め打ちで行きます。そのため以下のようなものが必要となる想定
- cardId
- prefabのAssetName
以上だ!
今回はカードの種類が少ないのと時間がないので、カード1つ1つをprefabで用意してみたいと思います。
拡張性は薄いですが、cardIdからprefabを生成できる。prefabがカード効果を持ってるので難しい事考えずに行けると思う。
カードprefabの作成
というわけで生成の肝となるprefabを作っていきます。
こんな感じ。
名前やテキストはprefabに決め打ちで入力して、カード効果はenumを用意することにします。
これはカードの種類分prefab作成して、ScriptableObjectのListからロードできるように用意する!
上記は基本となるベースのprefabなので、派生prefabを作る際はプレハブバリアントで作成してみる。
継承の考え方よね?ならそれがきっと良さそうだ!
作り方はベースとなるprefabを右クリックして「プレハブバリアント」を選択。
これでCardBaseプレハブをベースとした新たなprefabが作れる。ジャンジャン量産するぞ!
フィールド要素の実装
カードのprefabが完成し終えたら、次はフィールド要素を順次実装していく。
必要な要素は
- 手札エリア
- フィールドエリア(カードを出す場所)
- 山札表示
- プレイヤー表示
- 対戦相手表示
- 勝利条件表示
- 場の「ない」「ある」情報表示
- 対戦相手の情報表示
- 残り体力
- ターンエンドボタン
- ラウンド数
あたりか。かなり要素が多めだ~~~!
というわけでまずは要素を配置してUIレイアウトを構築していこう
UIレイアウトの構築
やる事は単純!
上記の要素の配置を考えてグレーボックスを置くだけ。早速やってみよう
出来た!これがバトル面の基本のUIとなるので、ここから1つ1つの要素を実装していく
まずは手札から実装していこう
手札実装
というわけで手札を作っていきます。挙動は以下のもの!
- カードにカーソルを合わせると拡大しつつ前面に表示する(情報確認のため)
- クリックしたらカードを場にだす。その際カード処理を実行する
- 手札が増減された場合、手札エリアで整列をする
という所でしょうか。まずはマウスオーバーとクリックのイベントハンドラを仕込んでいくぅ!
手札にマウスオーバー時、カードの拡大表示を行う
基本のこれ。情報確認するためにも拡大しないと読みづらいよね。
カードはPrefabで作ったので、そのrootとなるGameObjectにイベントを実装していく。
既にUnityに用意されているみたいなのでスクリプトにIPointerEnterHandlerと
IPointerExitHandlerを継承させ、それぞれOnPointerEnter(PointerEventData eventData)と
OnPointerExit(PointerEventData eventData)の2つの関数を実装すれば後はいい感じにしてくれるみたい。
これどこでどういう判断して検知してるんだろう?子オブジェクトの範囲もレイキャストで取ってるのか、それとも張り付けたオブジェクトのtransformを参照しているだけかな?
続いては重なり合った時にマウスオーバーしたカードを最前面に表示する処理。
どうやらUnityではそういうorderを設定できる項目があるみたいだが、調べてみてもなかなか同一Canvas内での重なりを制御する方法はなさそう。
同一CanvasではsiblingIndexを操作してヒエラルキーの順序を一番下にするのが一般的みたい。
じゃあそうしてみるか!
というわけで手札を整列させる処理を作ったのであった。やったね。
ついでに弧を描くように実装してみた!あんまり多すぎると形は整わないけどこんな感じ。
//手札の整列具合の設定
[SerializeField] protected float radius = 1000.0f; // 弧のカーブ具合を調整するパラメータ
[SerializeField] protected float angleStep = 3.0f; // 配置するカード間の1枚毎の角度を設定
[SerializeField] protected float maxAngle = 60.0f; // 最大角度
[SerializeField] protected float moveTime = 0.2f;
protected void SortHandCard()
{
int cardCount = cardList.Count;
float sortMaxAngle = Mathf.Min((cardCount - 1) * (angleStep / 2.0f), maxAngle);
for (int i = 0; i < cardCount; i++)
{
var card = cardList[i];
RectTransform rectTransform = card.GetComponent<RectTransform>();
if (rectTransform == null)
{
continue;
}
// カードの角度計算(中心を基準に左右対称になる)
float angle = -sortMaxAngle + angleStep * i;
float radian = angle * Mathf.Deg2Rad;
// 弧に沿った位置を計算
Vector2 position = new Vector2(
Mathf.Sin(radian) * radius,
Mathf.Cos(radian) * radius - radius // 半径分上方向を調整
);
// カードのRectTransformを更新
rectTransform.DOAnchorPos(position, moveTime);
// 回転を設定(弧に沿ってカードが曲がるようにする)
rectTransform.DORotate(new Vector3(0, 0, -angle), moveTime);
}
}
ゲームマネージャー実装
インゲーム中は以下の情報を戦闘またいで引き継ぐ必要があります
- デッキ
- スコア情報
- 残りライフ
- ラウンド情報
これらを管理するため、ゲームマネージャーで情報を格納するよ!
デッキ用のScriptableObjectを実装
もうこればっかり!初期手札を設定したりチュートリアル用の手札を設定したりと色々使い道があるので作っていきます。
必要な項目はシンプル。カードの種類のList、これのみ!
対戦相手のScriptableObjectの実装
最初はたくさん作ろうと思ったのですが、時間もないので決め打ちで作っちゃいます。
必要な情報は以下の通り
- 画像アセット名
- クリア条件1,2,3
- クリア報酬1,2,3
クリア条件は乱数で幅を用意させます。それでプレイ感を若干味変。難易度が難しすぎる事もありますがそこはご愛敬。
対戦相手の初期化
カードの初期化が出来たらこっちもやらないとね!先ほど作ったScriptableObjectを受け取ったら戦闘シーンの各種情報に代入していきます。
対戦相手の情報や勝利条件などの変数を格納していくために、
エネミー情報GameObjectを実装し、そこにスクリプトを当てて作っていく想定。
場の情報の更新
フィールドに出したカードの情報を場で保存するような処理を作ります。
場に出ているカードの数値を集めるゲームなのでこういうの大事!
ひたすらSerializeFieldを作ってそこの更新処理をつないでいく実装をしていきます……連結が大変……
戦闘初期化処理
戦闘が始まったらデッキを手札に格納してシャッフルしないとね!
というわけで作っていきます。先ほど作成したScriptableObjectを受け取って、その情報を基に山札リストを作ってシャッフルさせて上から配る感じ!いけるいける!
カード実行処理
だいぶシンプルなルールなのでカードをクリックしたら場に出るようにするだけ。
捨て札とかそういう管理はやってる時間がないのでシンプル実装だ!
山札確認処理
多分これだけは必要。山札に含まれているカードを表示するためのUI作ります。
予めUIを用意して、クリックされたらそれを表示。という流れにする予定。
本来ならスクロールバーなどで賢く実装するのがいいのだろうけど、今回は簡易的なものなので
各種類のカードがどれぐらい残ってるかを表示するにとどめる予定。
こんな感じに出来た!
ターン終了&クリア条件判定処理
最後にカードを出し切ってターンを終了し、判定をチェックして勝利か敗北かに移す処理を実装します。
ターン終了時には演出が入るのでそれも重要!やる事が多い!
ターン終了時にはカットインを表示します。そのためにはカットイン用のUIを作ってボタン押下時にそれをActiveにしつつアニメーションをする感じ。
何とか出来た……形を配置したらDOTweenとUniTaskで適当にイージング決めてアニメーションさせてます。お手軽簡易カットイン
勝利・敗北演出
後はターン終了時に勝利判定をして勝利演出と敗北演出を出します!
それぞれ勝利用、敗北用のUIを作ってそれを出しわけるのがお手軽な実装と学んだのでそちらで実装。
なんとか出来た!
これで一通りのインゲーム処理が揃った。
残りは
- バトル報酬画面
- リザルト画面
- ステージのバランス設定
- タイトル
- エンディング
あたりを盛り込めば終わりだ!
いよいよ完成が見えてきたぞ!!!
バトル報酬画面
ステージとステージの合間ではカード報酬をもらえて、それでデッキを強化していきます。
そんな報酬画面を作るぞ!省エネで作る!!
報酬は4種類から1つを選ぶタイプ。スキップも出来る。
そんな感じで進んでいきます。
というわけでサクッと作った!
やり方は簡単で、ボタンを4つ用意したらそれぞれにインデックスを振り、
押されたインデックスの報酬をリストに格納する、というやり方でやってます。
報酬は被らないように重複したデータをリストの形で作って、シャッフル後に先頭の4つのみを抜き出すというやり方で抽選。
リザルト画面実装
これはもうゲームクリア、ゲームオーバー時にスコアとかの情報を参照できるようにして遷移するだけ!
後でランキングも入れたいのでランキング送信とタイトルに戻るボタンを用意して準備しておきます
リザルトも出来た!イエイ!
チュートリアル実装
これが一番重い!チュートリアルフラグを立てたらバトルシーンの上にモーダルレイヤーを追加します。
このモーダルはクリックされたらセリフ送りをするシステムで実装して、
後は適当なタイミングでカードやボタンのクリックを抑制したり、該当箇所を光らせたりする。
それの繰り返し!複雑な割にはごり押しで実装しているので語る事もないよ
(時間があったらもっと分かりやすくて汎用的なチュートリアル作りたいね……)
うわー!何とか出来たー!間に合えー!
シナリオ作る
やってなかった!やります!!
タイトル作る
これも大事!ロゴとか作って配置します!
出来た!間に合えー!
ランキングを実装する
うわーー!時間ねー!!
ここ!ここ見る!!
git入れておけば楽なのでそうしておく!!!
WebGLでビルドする
急げ急げー
ハマりポイント
Unity6はBuild Settingsがなく「Build Profiling」になっている。
やる事は同じなのでWebを選んでSwitch Buildする。
プロジェクト設定のUNITASK_DOTWEEN_SUPPORTは忘れないように再設定する
あー大失敗
unityroom addressableダメだった……
ああーーーー聞いたことがある気がするのに
選んじまった……
今から全部Resorucesに差し替えます
差し替えて再ビルド
地獄だった
Addressableのフォルダを丸々Resourcesにコピーして
全部Resources.LoadAsync関数に差し替え。
うわあああああああ!!!
完全に遅刻ルートです。せめてものあがきで今日中に出したいね。
何とか完成。そしてアップロードへ
長く苦しい戦いだった……
というわけで!私のUnity1week初挑戦は終わりました!
最初にWebGLビルドするべきだった!!!!第一の反省点はこれですね。
それ以外は大体好調だったと思います。
遅刻はしたものの、完成までほぼ1週間でこぎつけたので満足です。
後は遊んで貰えるといいな……どうかな……歓迎されるかな……
URLはこちら