ADVENT CALENDAR

Altseed2に移行してみた話

By zamaka

はじめに

Amusement Creators Advent Calender 2020 2日目の記事です。
筆者は「対戦パズル~しき~」というゲームを開発し始めて、気が付けばかれこれ3年目です。
Altseed2 が公開されたので、上記ゲームプロジェクトを初代 Altseed から試しに移行してみました。
その忘備録兼情報共有の記事になります。
Altseed2 気になってるけど、初代のゲームの続き作ってるしなぁ、という人がいれば、参考になるかもしれない、そんな記事です。

前提

環境はWindows + VisualStudio2019です。
読者は初代 Altseed のことは理解しているが、2 はまだほとんど触ってないという人を想定しています。
本記事の情報は2020年11月時点のものになります。今後 Altseed2 の開発が進むにつれて情報が古くなる可能性は高いです。
また、該当ソリューションは Altseed を使った描画部分(View)と、ゲームロジック部分(Model)として複数プロジェクトに分けてあります。 自分が使用していた機能についての言及となるため、漏れが山ほどある(例えばゲームの都合上、カメラオブジェクトはほぼ使ってない)ので、情報は参考程度に、各自で最新のリファレンスを参照してください。

手順

実際に行った手順を紹介します。

1. ブランチを生やす

まず、移行が上手くいかない場合に備えて、Gitのブランチを切りました。
もちろん必須ではありませんが、この手の大きめの破壊的変更を試験的に行う場合は、いざというときの安心感が違いますし、後から変更を無かったことにしたり、変更箇所を見比べることもできて便利です。

2. .csprojの変更

本記事を参考に、実際に移行する場合、.csprojを変更する前に、先に一通りの流れを把握することを強くお勧めします(参照を変えた後だと、しばらくコンパイルエラーと戦うことになるため) 。
任意の方法で Altseed2 を参照するわけですが、公式サイトでも紹介されている Nuget を利用しました。
もともと初代 Altseed を Nuget で導入していたので .csproj ファイルに<PackageReference Include="AltseedDotNet"/>がありました。 これが<PackageReference Include="Altseed2"/>になった形です。
また、TargetFramework も変更します .NetFramework から .NetCore になりますね。 この変更でもエラーが発生する場合が有るので適宜修正しましょう1。 .csproj の書き換えは、2のチュートリアルを見ながら新規プロジェクトを作って、既存の .csproj ファイル2と見比べると良いです。

3. 名前空間の置き換え

この時点で大量のコンパイルエラーが発生しているはずですが、めげずにちまちま置換していきます。
まずは asd から Altseed2 に名前空間を置き換えます。
これまで短くてタイプしやすいからと、asd. を乱用していたので、これを機に、まずは using を利用するように整理しました。
名前空間を using に移動する場合、VisualStudio の機能であるクイックアクションを使うと便利です。
後はusing asd;using Altseed2;として置換を行えば完了です。 クラス名の被りが発生した箇所3のみ、個別に対応する必要はあります。

4. 機械的な置き換え

次に使い方がほぼ同じで、機械的な置換で対応できる箇所を置換していきます。
置換する際には、正規表現をうまく活用すると作業時間が短縮できます。
代表的なところや、覚えている限りで以下になります(asd 及び Altseed2 名前空間は省略してあります)。

全体

Altseed Altseed2
EngineOption Configuration
EngineOption.IsFullScreen Configuration.IsFullscreen
EngineOption.IsWindowResizable Configuration.IsResizable
Vector2DF Vector2F
Vector2DI Vector2I

表示系

Altseed Altseed2
TextureObject2D SpriteNode
TextObject2D TextNode
○○.DrawningPriority ○○.ZOrder
Engine.Graphics.CreateTexture2D() Texture2D.Load()4

Altseed Altseed2
SoundSource Sound
Engine.Sound.CreateSoundSource() Sound.Load()4

図形

初代ではGeometryObjectShapeRectangleShape等の形状を指定していましたが、形状毎にRectangleNode等の独立したノードになりました。他の図形も同様です。
使用感はほぼ同じですが、一部プロパティ名も変更されています。

5. 手作業を伴う置き換え

ここからが問題にして本題。使用方法や設計が変更されていて、機械的な置換ができないクラス等について。 Altseed2 では親子関係を持つ多くの概念が、Nodeに統合されています。
そのため、使い方によっては変更が必要になります。 従来の感覚でSceneLayerを使いたい場合、Nodeを継承してSceneLayerクラスを自作すると変更箇所を減らせるかもしれません。

シーン

子要素(レイヤー)の登録方法が変更されるほか、OnHogehoge()が変更になります。

Altseed Altseed2
Scene Node
Scene.AddLayer() Node.AddChildNode()
Scene.RemoveLayer() Node.RemoveChildNode()
Scene.OnRegistered() Node.OnAdded()
Scene.OnUpdating() Node.OnUpdate()5
Scene.OnUpdated() Node.OnUpdate()5
Scene.OnUnregistered() Node.OnRemoved()

シーンチェンジを行う場合、一瞬で切り替わって良ければ、Engine.AddNode(), Engine.RemoveNode()で良いと思いますが、
トランジションをしたい場合等はTransitionNodeを用いるようです。 従来のOnStartUpdating()Scene.TransitionBegin()等のシーン開始・終了時の処理も TransitionNode.OnNodeSwapped()TransitionNode.OnTransitionBegin()等を用いることで実現できそうです(未確認)。

レイヤー

シーン同様に各メソッドが変更となる他、ポストエフェクトの扱いが少し変わっています。

Altseed Altseed2
Layer2D Node
Layer2D.AddObject() Node.AddChildNode()
Layer2D.RemoveObject() Node.RemoveChildNode()
Layer.OnUpdating() Node.OnUpdate()5
Layer.OnUpdated() Node.OnUpdate()5
Layer.AddPostEffect() Node.AddChildNode(new PostEffectNode)

ポストエフェクトもノードになっているので、初代より細かい制御ができそうです。 とりあえず、LightBloom はほぼそのまま使えました。

コンポーネント

コンポーネントは廃止され、Nodeに統合されました。AddChildNode()で子ノードとして登録し、Ownerの代わりにParentを使えば何とかなると思います。
複数のコンポーネントを登録名で識別・取得していた場合は、専用の Component クラスを自作する等の対応が必要です。

オブジェクト

オブジェクトについて。TextureObject2DTextObject2Dそのものは前述の置換だけで、ほぼそのままの使用感で使えます。 注意が必要なのは主に2点。親子関係と、描画に関するインターフェース周りです。

一つ目の注意点、親子関係については、親子間で何を引き継ぐかのオプションが無くなっています。具体的にはOnject2D.AddChild(Object2D, ChildManagementMode, ChildTransformingMode)だったのが、
Node.AddChildNode(Node)となり、引数が減っています。
Altseed2 の子ノードは、(ChildManagementMode)0b1111, ChildTransformingMode.Allに相当し、すべて親から引き継ぐ設定とみなせます。
一方で、Onject2D.AddDrawnChild()相当のメソッドは無く、IsDrawnColorについては引き継がれません。

二つ目の注意点はDrawnObject2Dが無くなっている点です。 SpriteNodeTextNodeが混在する場合にIsDrawnColorをまとめて操作することができません。

これらの問題の解決策は後述します。

入力周り

割と機械的な置換で対応できますが、ジョイスティック関連のメソッドは、id を使う方式になり、引数等が変わっています。 また、地味にButtonStateの順番が変更になっているので、int にキャストしてる場合等は注意が必要です。

Altseed Altseed2
Keys. Key.
Engine.JoystickContainer.GetIsPresentAt() Engine.Joystick.IsPresent()
JoyStick.Get○○State() Engine.Joystick.Get○○State()
Joystick.JoystickName JoystickInfo.Name

文字表示

TextNode及びFontに関しても、いくつかの仕様変更があり機械的な置換だけでは対応できません。
発生した問題は、大きく以下の3つです。
Font.CalcTextureSize()の代替は、TextNode.ContentSizeだが、コンストラクター等では未計算(Update されてから取得する必要がある)
・ダイナミックフォント生成時に色や輪郭線を指定できない
.otfのオープンタイプフォントは上手く表示できない不具合
特に最後の不具合はエンジン利用側ではどうしようもなかったので、一旦放置しました。

6. オブジェクト周りの解決策

一つ目、親子で登録状況とトランスフォームを独立させたい場合。
例としては、ChildTransformmingMode.Nothing等を使っており、登録状況は共有したいけど、座標は独立させたい場合6やその逆の場合です。
この場合、「登録状況を管理する親ノードを、新たに作成する」「子ノードとして登録はせず、参照をもって管理する」等の解決策が考えられます。

二つ目、IsDrawn等を一括操作したい場合。
自分はTransformNode拡張メソッドを実装しました。 以下が一例です。(Colorについても同様)

static class DrawnNodeExtention
{
	public static bool GetIsDrawn(this TransformNode drawnNode)
        {
            return drawnNode switch
            {
                SpriteNode node => node.IsDrawn,
                TextNode node => node.IsDrawn,
                ArcNode node => node.IsDrawn,
                CircleNode node => node.IsDrawn,
                LineNode node => node.IsDrawn,
                RectangleNode node => node.IsDrawn,
                TriangleNode node => node.IsDrawn,
                _ => false,
            };
        }

        public static void SetIsDrawn(this TransformNode drawnNode, bool value)
        {
            switch (drawnNode)
            {
                case SpriteNode node:
                    node.IsDrawn = value;
                    break;

                case TextNode node:
                    node.IsDrawn = value;
                    break;

                case ArcNode node:
                    node.IsDrawn = value;
                    break;

                case CircleNode node:
                    node.IsDrawn = value;
                    break;

                case LineNode node:
                    node.IsDrawn = value;
                    break;

                case RectangleNode node:
                    node.IsDrawn = value;
                    break;

                case TriangleNode node:
                    node.IsDrawn = value;
                    break;

                default:
                    break;
            }
        }
}

ただし、これだとCameraNode等でも、上記の Get Set を呼べてしまうため、設計としては微妙です。 IDrawnNodeインターフェースを自作したうえで、SpriteNodeIDrawnNodeを継承したクラスも自作し、SpriteNodeの代わりにそのクラスを利用する(TextNode等も同様)…とかの方が良さそうです。
今回は妥協しました。こういうこと放置して忘れると、後でリファクタリングが大変になるわけですが。

三つ目、親の描画状況を引き継ぎたい場合。
親ノードをたどってIsDrawnを更新する処理を自作する必要があります7
上記の拡張メソッドを自作した場合、基本はIsDrawn = Parent.GetIsDrawn()のように書けます8。適用箇所が多い場合は、コンポーネントのようにするのが良いかもしれません。

結果

自分が対応したのは大体このあたりになります。その他、細かい箇所も雑にコメントアウト対応し、コンパイルエラーは消しました。
が、実行してみると、前述の TextNode や IsDrawn 周りの影響で、いくつか表示が乱れたままであることが判明。
さらにシーン切り替え時の読み込みが、初代と比べて長い(一瞬だったのが、数秒かかる)という問題が発覚しました。
この辺りで、「Altseed2 の開発が進むのに期待してもう少し待つかー」と、結局元のブランチに帰ったというのが、今回の結果です。
作業時間自体はたぶん数時間程度なので、(学生個人にしては)大きめの規模のゲームでも休日1日使うつもりなら移行できるかなといったところ。

感想

Node 周りの仕様変更こそ大きいですが、基本的には従来に近い使用感で使えそうなので、エンジンの使い方を0から習得しなおす必要はなく、移行作業自体も機械的な置換が大半で、想像以上にスムーズに進みました。
今回は単純な移行を目的としたので、Altseed2 の新機能にはほとんど触れませんでしたが、シェーダーを中心とした描画処理の柔軟性は、初代と比べて増していると思うので、そのあたりを早く導入してみたいです。
しかし、OTF フォント周りだけは、設計で誤魔化せないので、残念ながら今すぐ移行するのは厳しいです。そこが直ったら本格的に移行したいところ。
また、結局移行こそ見送ったものの、名前空間の整理や、各種設計の見直しをついでに行うことになり、リファクタリングのいい機会になりました。Altseed に限らず、ゲームエンジンの変更が簡単にできる状態を目指すと、ゲームロジックと、描画などの処理がきれいに分離された良い設計に近づくと思います。

最後に、Altseed2開発メンバーは日々お疲れ様です。


  1. 詳細は調べてませんが、セーブデータの使用言語として保存していた、System.Globalization.CultureInfoがシリアライズできなくなったので、カルチャー名の文字列を保存するように変更しました。 [return]
  2. なお、.csproj ファイルには新形式と旧形式が存在するため、旧形式からの移行だと結構めんどくさいです。ネットで調べれば情報は出てくるので頑張りましょう。 [return]
  3. 自分の場合、自作したCursorが被ってました。 [return]
  4. 読み込み失敗時に例外を出す、LoadStrict()も適宜使うと良さげ。 [return]
  5. OnUpdating()OnUpdated()を使い分けていた場合は注意 [return]
  6. キャラクターと、関連 UI をひとまとめに登録管理したい場合や、フィールド設置型の弾を生成者と一緒に消滅させたい場合等。 [return]
  7. どうやらIsDrawnActuallyが機能していない(?)ようなので、バグor実装中の可能性もありそう。 [return]
  8. 親の親から引き継ぎたいとか、自身の状態によっても描画状態が変わるとかだとひと手間必要。また、ノードの更新順によっては意図しない結果になる可能性もある。 [return]

SHARE THIS POST