Help us understand the problem. What is going on with this article?

C# 用ゲームエンジンを自作した話

これは、C# その2 Advent Calendar 2019 の9日目の記事です。

あまり記事を書かない人間なので、駄文失礼します。

Unity、使っていますか?

Unity、便利ですよね。コードを書かなくてもオブジェクトを配置して簡単に3Dゲームを作れちゃいます。ポケモン GO やデレステなど著名なスマホゲームにも使用されていて、今や C# でゲームづくりといえば Unity の名前が挙がらないことは無いでしょう。

そう、3D ゲーム開発においては……。

2D、つらい。

Unity で 2D に重きを置いたゲームを作るのはかなり苦行です。これは1年以上前にUnityをやめた僕の感想なので、今の Unity 事情は正直わかりませんが、当時タイルマップが実装されたときは、かなり頑張って Unity で 2D ゲーム開発していました。

しかし、どうしてもピクセルパーフェクトにするのが大変だったり、昨今のあらゆる画面解像度に 2D の画面を適合させたりするのが死ぬほど辛かった…。ピクセルパーフェクトにするためにシェーダーを使う手法を取り入れたりしましたが、シンプルに画面上にドット絵を表示するためだけに、わざわざ 3D 空間の描画に最適化されたレンダラーにお手製シェーダーをぶっこんで、2D を綺麗に描画するって何か無駄でしかなくてそれもまた辛かったです。

Unity は 3D ゲームを作るためのソフトウェアなので、ここでいっそ Unity ではないゲームエンジンを採用することにしました。

C# 向けの他のゲームエンジン

C# で使えるゲームエンジンは意外とたくさんあります。

MonoGame

http://www.monogame.net/

MonoGame は、Microsoft XNAをクローンしたゲームエンジンです。とても完成度が高く、クロスプラットフォームだそうです。

とはいえ、ドキュメントがあまり多くなく、また機能の多さゆえにとっつきにくい印象を受けました。

CocosSharp

https://github.com/mono/cocossharp

Cocos2DのC#移植です。その名の通り2Dに特化しています。とはいえ、4,5年前から音沙汰がなく、死んだプロジェクトっぽいです。

ゲームエンジンを自作した

いろいろ探してみましたが、やはり納得のいく、自分の求めるゲームエンジンは見つかりませんでした。
もはや、ゲームエンジンを一から作ってしまったほうが早いかなって思い、作ってしまいました
その名も、DotFeather。

DotFeather

https://github.com/xeltica/dotfeather

DotFeather は、2Dに特化軽くてパワフルなゲームエンジンです。
.NET Standard 2.1 に対応しており、C# 8ではヌル安全に書けるようになっています。
OpenTK と、一部の画像処理にImageSharpを使用しています。

備えている機能は以下の通りです。

  • グラフィック要素
    • スプライト - 画面上へのテクスチャ表示
    • タイルマップ - テクスチャを敷き詰めたマップ表示
    • グラフィック - 線分や矩形を1ピクセル単位で描画
    • テキスト - 文字列を描画できるオブジェクト
    • コンテナー - グラフィック要素を格納できるオブジェクト
    • アニメーティングスプライト - アニメーション可能なスプライト
    • 9スライススプライト - テクスチャを9分割して、矩形状のテクスチャ−をスムーズに引き伸ばせる特殊なスプライト
  • キーボード入力
  • マウス入力
  • シーン遷移機能
  • 音楽再生
  • 効果音再生
  • シングルスレッドで疑似的に非同期できる、コルーチンシステム
  • 高い拡張性
    • 独自の描画機能の追加
    • タイルマップ用の独自タイル追加
    • オーディオ機能の拡張

ver2.5時点で、本格的な2Dゲームを制作するための十分な機能が揃っています。

使ってみる

DotFeather は nuget を用いて簡単に入手できます。

Visual Studio で

パッケージマネージャーで「DotFeather」と検索します。

dotnet CLI で

dotnet add package DotFeather

Hello, world!

クラスを一つ作ります。

Game.cs
using System;
using DotFeather;

public class Game : GameBase
{
    public Game(int width, int height) : base(width, height) { }

    protected override void OnLoad(object sender, EventArgs e)
    {
        Print("Hello, world!");
    }

    static void Main()
    {
        new Game(640, 480).Run();
    }
}

実行すると、次のように表示されます。

image.png

応用編

ちょっと踏み込んで、イチゴがマウスカーソルを追いかけるロジックを組んでみましょう。

まず、イチゴの画像ファイルをダウンロードしてください。
ichigo.png
このファイルを、「ichigo.png」という名前で、プロジェクトに追加します。ファイルをプロジェクトに追加する方法は別途お調べください。

先程のプログラムを次のように書き換えます。

Game.cs
using System;
using DotFeather;

public class Game : GameBase
{
    public Game(int width, int height) : base(width, height, "いちご", 60, false, true) { }

    protected override void OnLoad(object sender, EventArgs e)
    {
        ichigo = Sprite.LoadFrom("./ichigo.png");
        ichigo.Scale *= 2;
        Root.Add(ichigo);
    }

    protected override void OnUpdate(object sender, DFEventArgs e)
    {
        var mouse = DFMouse.Position;
        var angle = Vector.Angle(ichigo.Location, mouse);

        ichigo.Location += new Vector(MathF.Cos(angle), MathF.Sin(angle)) * 256 * Time.DeltaTime;
    }

    static void Main()
    {
        new Game(640, 480).Run();
    }

    private Sprite ichigo;
}

OnUpdate() というメソッドが増えたのがわかるかと思います。
勘の良い方ならお察しかと思いますが、OnLoad() メソッドは、最初に一度だけ呼び出されます。このメソッドには、本来その名の通り、画像データや音楽データなど、必要なリソースを初期化するために用意されています。

ichigo = Sprite.LoadFrom("./ichigo.png");
ichigo.Scale *= 2;

:strawberry:の画像を読み込んでスプライトを作成しています。スプライトとは、ゲーム画面を自由に動き回る画像オブジェクトのことです。
さらに、そのままだと小さいので:strawberry:のサイズを2倍しています。2行目の数値を変えることで、自在に伸縮できます。

Root.Add(ichigo);

生成した:strawberry:はそのままでは画面に表示されないので、画面に追加します。Rootオブジェクトに追加するだけで描画してくれます。

var mouse = DFMouse.Position;

DFMouse 静的クラスは、その名の通り、マウス入力に関するAPIを提供します。この行では、マウスカーソルの座標を2次元ベクター型として取得しています。

var angle = Vector.Angle(ichigo.Location, mouse);

ichigo.Location プロパティは、:strawberry:の現在の座標を2次元ベクター型で取得できます。
Vector.Angle(Vector, Vector) 静的メソッドは、2座標間の角度をラジアン単位で取得できます。

ichigo.Location += new Vector(MathF.Cos(angle), MathF.Sin(angle)) * 256 * Time.DeltaTime;

あとは、先程得た角度からコサイン・サイン関数を用いて速度に変換し、:strawberry:を移動させます。
Time.DeltaTime 静的プロパティは、前のフレームと現在のフレームの間にかかった時間を秒単位で取得します。これを乗じることで、フレームレートが落ちても変わらない速度で:strawberry:を移動させられます。

実行すると、:strawberry:が素早くカーソルを追いかける怖い画面が完成します :tada:

Dec-08-2019 12-42-56.gif

このようにシンプルに記述してすぐ動かせる魅力的なゲームエンジンとなっています。さらにリポジトリにはデモが用意されているので、ぜひクローン、ビルドして遊んでみてください。

スマホ対応へ

DotFeather は現在PCにしか対応していませんが、Xamarin.Forms を使ったスマートデバイス版である、DotFeather.Mobile の開発を現在続けています。

スマートデバイス固有の新機能も予定しています。

  • マルチタッチ入力 DFTouch
  • タッチスクリーンによる DFMouse のエミュレーション
  • 加速度センサー DFAccel
  • ジャイロセンサー DFGyro

さいごに

宣伝みたいな記事になってしまいましたが、これを読んでいただいた方にDotFeatherでゲームを作ってみてもらえるととても幸いです。

日本人デベロッパーによるC#製ゲームエンジンという分類ではおそらく現時点で一番完成度の高いモノであると自負しています。GitHub上でのバグ報告や、質問なども気軽に受け付けていますし、公式Discordもあるのでサポートも万全です。

タイルマップ用のウディタ互換オートタイルプラグインも作ったので、ぜひ。

ここまで見ていただきありがとうございます。それでは。

2019/12/9 追記: リポジトリのURLを貼りました。

Xeltica
C# / Unity / Xamarin / TypeScript / node.js / Koa / Vue / Sass / Pug
https://xeltica.work
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした