ぽりへどろんの初心者ゲーム開発日記

ぽんこつプログラマが同人ゲームを作りながら調べたこと考えたことを書くブログ

自作ゲーム制作エンジン「SplitterSprite」について

ゲーム制作がひと段落したので、自分が長期的にやろうとしていることを文書の形で整理するよ。

自作ゲーム制作エンジン「SplitterSprite」について

TL; DR

  1. いろんなアイデアの同人ゲームが増えてほしいと思ってるよ
  2. それにはゲーム制作ツールじゃなくて自作プログラムのゲームがいいと思ってるよ
  3. 自作プログラムのゲームを簡単に作れるようにゲームのプログラム・素材を公開・共有する仕組みが必要だよ
  4. それを実現するためのプログラムSplitterSpriteを作ってるよ

いろんなアイデアの同人ゲームが好き

私は、いろんなアイデアの同人ゲームがもっと世に増えてほしいです。

私は同人活動が好きです。自分でするのも、人がしているのを見るのも好き。 同人にはいろんな媒体があります。メジャーなところはイラスト、マンガ。アニメの自主製作。ほかにもいろいろ。中でも私はゲームという媒体が好きです。

ゲームのいいところは、プレイヤーにいろんな選択肢があるところ。ほかの媒体はどうしてもストーリーが一本道になりがちだけど、ゲームはプレイヤーの選択でストーリーを変化させることができる。それにストーリー本編に関係ないところで、関係ない遊び方だってできる。 できのいいゲームだと、世界のありようを隅々まで遊びつくしたくなる、何十時間かけてでも。 もちろん、シンプルなゲームも好き。シンプルなゲームもハイスコア目指して、一日中やっちゃったりします。

そんな私なので、もっと世に同人ゲームが増えてほしいな、と思うのです。 さらに言えば、作者のアイデアが存分に反映されたようなゲームが増えてほしいのです。

イデアの実現には自作プログラム

では、いろんなアイデアの同人ゲームが増えるには?私は、自作のプログラムで実装されたゲームが作りやすくなることが大事だと考えています。

まず、今のゲームの作り方ってどんな感じでしょう?それは、私の知る限り以下の2つに分類できます。

  1. ゲーム制作ツールを使う
    • 概要:RPGツクールとか吉里吉里のようなツールで作成するということです。
    • 利点:プログラムの作成コストが低い。素材(※1)も豊富。
    • 欠点:特殊な処理を実装するには厳しい。(※2)
  2. ゲーム制作ツールを使わない
    • 概要:プログラミング言語で自作プログラムを組むということです。Unityなどのエンジンを使用することも含みます。
    • 利点:特殊な処理を実装できる。
    • 欠点:プログラムの作成コストが高い。素材の準備が必要。

※1:ここでは素材とは、画像、音声、テキスト、エフェクト、モーション、フォント、マップ、パラメータ設定を指すこととします。

※2:RPGツクールでも特殊処理の実装にスクリプトを使えるものがあると聞きますが、やはりプログラミング言語で作ったほうが処理は実装しやすいでしょう。あくまで主観ですが。

もちろん、私はどちらで作られたゲームも好きです。 ただし、作者のアイデア次第でいろんなゲームが生まれるのは後者「ゲーム制作ツールを使わない」、自作プログラムで実装されたゲームだと思っています。

自作プログラムゲームって大変

自作プログラムのゲームが作りやすくなってほしいとしたら、現状、何が問題でしょう?それは先に上げた欠点である、「プログラムの作成コストが高い」ことと、「素材の準備が必要」なことが問題だと思います。

本当だったら、作者さんの「独自のアイデアの部分のプログラムだけ」、「独自に使いたい素材だけ」を作者さんが作れれば理想なのに、自作プログラムでやろうとすると全部作者さんが準備することになっちゃう。

これはすごく大変です。ゲームを作ろうと思っても時間がかかりすぎてしまいます。

しかも、いろんな作者さんが、それぞれ独自でない部分を作ることになります。似たようなものをいろんな作者さんが作ることになります。これは、無駄が大きいです。

公開しよう、共有しよう

この問題の解決には、どうすればいいでしょうか。それには、作ったプログラム、素材が公開され、共有されるようになればいいのです。

プログラムと素材が共有されていれば、独自でない部分は共有されているものを組み合わせて、作者さんは独自部分だけを作ればゲームを完成できます。

動画制作においては、このような文化が既にあります。例えば、ニコニ・コモンズは動画作成の素材が共有されるサイトですし、MMDではモデル素材・エフェクト素材が公開され、動画作者の方がダウンロードして動画制作を行うことができます。素晴らしいです。

プログラムにおいても、このような文化があります。オープンソースソフトウェアと呼ばれるもので、作成したプログラムを公開し、そのプログラムを利用したいプログラマはダウンロードして使うことができます。フリーソフトと異なるのはプログラムの中身も公開するので、ダウンロードした人は改造して利用できる点ですね。(※3)

※3:公開ライセンスにもよります

ゲーム制作においても、こういった文化ができると自作プログラムでのゲーム作成がぐっと作りやすくなると思います。

ゲーム制作で公開・共有文化を作るには?

ゲーム制作において、プログラムと素材の公開と共有は現状容易でしょうか?いいえ。そのためには、ゲーム制作に用いるプログラムと素材の規格統一が必要です。

プログラムの公開と共有は可能です。GitHubというサイトにプログラムを公開すれば、全世界に共有できます。

素材の公開と共有も可能です。画像も音声もテキストもエフェクトもモーションもフォントもマップもパラメータ設定もウェブ上でファイルを公開すればよいのです。

でも、各作者様がバラバラのプログラミング言語でプログラムを書いていたら、せっかく公開されていても組み合わせられません。素材についてもファイルの形式がプログラムで読み込める形式になっていなければ、組み合わせられません。

つまり、ゲーム制作における公開・共有のための文化をつくるには、組み合わせやすいようにプログラミング言語・素材のファイル形式の規格を統一することが必要なのです。

SplitterSpriteエンジンによる規格統一

この規格統一のためのエンジンが今回制作中のSplitterSpriteです。

規格の統一方法を考えましょう。

プログラミング言語については、Javaとします。これはWindowsユーザもMacユーザもLinuxユーザも対象としたいためです。後述しますが、素材のファイル形式の規格統一のためにリフレクションを用いたいというのも理由です。

素材のファイル形式の統一について、シンプルな画像や音声ファイルはそのままpngやwav形式を使えばよいですが、エフェクトやマップ、パラメータ設定のような設定値を読み込む素材はどうでしょうか。これにはXMLを規格とします。

例として、ポケモン風のゲームを作成するための、次のようなクラスを考えてみます。

// ポケモン風のモンスターを表すクラス
public class Pokemonppoi {
    int hp; //HP
    int offence; //攻撃力
    int defense; //防御力
    int speed; //素早さ
    String explanation; //図鑑説明

    Pokemonppoi(int _hp, int _offence, int _defense,
                int _speed, String _explanation) {
        hp = _hp;
        offence = _offence;
        defense = _defense;
        speed = _speed;
        explanation = _explanation;
    }
}

このPokemonppoiクラスのインスタンスを生成するには「HP」「攻撃力」「防御力」「素早さ」「図鑑説明」のパラメータが必要です。 これに対して、以下のようなXMLを素材として扱うことがわかりやすいでしょう。

<root>
    <int field="HP">100</int>
    <int field="攻撃力">80</int>
    <int field="防御力">75</int>
    <int field="素早さ">120</int>
    <string field="図鑑説明">このモンスターはインド象よりもつよい</string>
</root>

しかし、素材から設定を読み込みたいクラスのすべてに対して、毎回XMLファイルからの読み込みプログラムを作成するのは面倒です。 そこで、私は今回XMLファイルの扱いのための以下の2つを作成しました。

  • Spiritクラス:XMLファイルからの読み込みをサポートするクラス
  • Spawnerインターフェース:Spiritクラスを使って対象のクラスインスタンスを生成するインターフェース

この2つを用いると、PokemonppoiクラスインスタンスXMLから生成するプログラムは以下のように書けます。

Public class PokemonppoiSpawner extends Spawner<Pokemonppoi> {
    int hp;
    int offence;
    int deffense;
    int speed;
    String explanation;

    PokemonppoiSpawner(Spirit spirit) {
        hp = spirit.intOf("HP");
        offence = spirit.intOf("攻撃力");
        deffense = spirit.intOf("防御力");
        speed = spirit.intOf("素早さ");
        explanation = spirit.stringOf("図鑑説明");
    }

    public Pokemonppoi spawn() {
        return new Pokemonppoi(hp, offence, deffense, speed, explanation);
    }
}

これにより、PokemonppoiSpawnerが実装されていれば、PokemonppoiクラスのためのXMLの形式が一意に定まります。 しかしながら、毎回XMLの設定値の内容を調べながらXMLを作成するのも大変です。 というわけでSpawnerインターフェースの内容をリフレクションで読み取り、XML編集を可能とするアプリ"Distiller"を作成しました。 以下のような画面イメージで編集が可能です。

f:id:polyhedron2:20180104080024p:plain

これらのSpiritクラス、Spawnerインターフェース、Distillerアプリを使うことで素材の公開・共有が可能になります。また、これらを含むプログラム群がSplitterSpriteエンジンです。

今回の例では、あるゲーム制作者さんがポケモン風ゲームを作って公開していたら、別の制作者さんがDistillerアプリでPokemonppoiSpawner用のXMLを作成して、モンスターの種類を追加したゲームをつくる、ということが可能です。

そして、SplitterSpriteによりゲームプログラムを実装してくださる方、XMLファイルを作成してくださる方が出てきてくだされば、ゲーム作成のための公開・共有文化ができていくのではないか……と期待しております。

おわりに

このような長文を最後までお読みくださった方、ありがとうございました。

SplitterSpriteはコミックマーケット93で1つのゲームを作成するところまで作ることができました。

今後はいくつかの種類のゲームをSplitterSpriteで作成しながら、使いやすさを向上していこうと考えています。

また、いずれはOSSとして公開したいと思います。

本記事に書いた内容に異論・反論あるかもしれません。その時は是非コメントいただけますと幸いです。

Scala: NothingとNullで遊ぼう

Scalaを使っていると触ることになるであろうNothingとNullで遊んでみた。

 

こちらを参考にいたしました。

Scala Anyクラスメモ(Hishidama's Scala Any Memo)

 

こちらによると

nullはNullというクラスの値になっており、Null型は全てのAnyRefの子クラスの下に位置づけられる。

であり、

AnyVal・AnyRefも含めて全てのクラスの子クラスとしてNothingが存在する。

とのこと。

Javaには「すべてのクラスの子クラス」というものはなかったので勉強も兼ねて遊んでみた。

 

1 Nothing型とNull型の持つメソッド・フィールドは?

「すべてのクラスの子クラス」ならば、Nothing型はAny型の想定しうる全メソッド・フィールドを、Null型はAnyRef型の想定しうる全メソッド・フィールドを、継承しているのか、という疑問が湧いた。

もっともそんなことができれば、無限種類のメソッド・フィールドを持っていることになるので、実際は違うはずである。

早速、REPLで試してみた。まずはメソッド。

scala> classOf[Nothing].getMethods.foreach(println)

public void java.lang.Throwable.printStackTrace()

public void java.lang.Throwable.printStackTrace(java.io.PrintWriter)

public void java.lang.Throwable.printStackTrace(java.io.PrintStream)

public synchronized java.lang.Throwable java.lang.Throwable.fillInStackTrace()

public synchronized java.lang.Throwable java.lang.Throwable.getCause()

public synchronized java.lang.Throwable java.lang.Throwable.initCause(java.lang.Throwable)

public java.lang.String java.lang.Throwable.toString()

public java.lang.String java.lang.Throwable.getMessage()

public java.lang.String java.lang.Throwable.getLocalizedMessage()

public java.lang.StackTraceElement java.lang.Throwable.getStackTrace()

public void java.lang.Throwable.setStackTrace(java.lang.StackTraceElement)

public final synchronized void java.lang.Throwable.addSuppressed(java.lang.Throwable)

public final synchronized java.lang.Throwable[] java.lang.Throwable.getSuppressed()

public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException

public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException

public final void java.lang.Object.wait() throws java.lang.InterruptedException

public boolean java.lang.Object.equals(java.lang.Object)

public native int java.lang.Object.hashCode()

public final native java.lang.Class java.lang.Object.getClass()

public final native void java.lang.Object.notify()

public final native void java.lang.Object.notifyAll() 

 

scala> classOf[Null].getMethods.foreach(println)

public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException

public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException

public final void java.lang.Object.wait() throws java.lang.InterruptedException

public boolean java.lang.Object.equals(java.lang.Object)

public java.lang.String java.lang.Object.toString()

public native int java.lang.Object.hashCode()

public final native java.lang.Class java.lang.Object.getClass()

public final native void java.lang.Object.notify()

public final native void java.lang.Object.notifyAll()

当たり前といえば、当たり前だけどすべてのメソッドは持っていなかった。

実際にnullの値から適当なメソッドを呼ぼうとしても怒られる。

scala> null.hoge()

<console>:12: error: value hoge is not a member of Null

       null.hoge()

続いて、フィールド。

scala> classOf[Null].getFields.foreach(println)

 

 

scala> classOf[Nothing].getFields.foreach(println)

 

それぞれフィールドは1つも持っていなかった。

今まで私は「クラスXがクラスYの子クラスであること」と「クラスXがクラスYを継承していること」が同値であると思っていて、「クラスYを継承したクラスXはYの全メソッド・フィールドを持っている」と思っていたが、どうにも違うということだ。

 

2 Nothing型とNull型の継承元は?

上の話を考えると「クラスXがクラスYの子クラスであること」と「クラスXがクラスYを継承していること」が同値ではないというのが実態なのだろう。

ではそれを確認するために、Nothing型とNull型の継承元を調べてみる。

scala> classOf[Nothing].getSuperclass

res8: Class[_] = class java.lang.Throwable

 

scala> classOf[Nothing].getInterfaces

res9: Array[Class[_]] = Array()

 

scala> classOf[Null].getSuperclass

res10: Class[_ >: Null] = class java.lang.Object

 

scala> classOf[Null].getInterfaces

res11: Array[Class[_]] = Array()

結果としてはNothing型はThrowableの子クラス、Null型はObject型の子クラスのようだ。それぞれ実装しているインターフェースは無し。

これを見る限り、確かに「クラスXがクラスYの子クラスであること」と「クラスXがクラスYを継承していること」は同値ではないようだ。

しかし、Nothing型がThrowableを継承しているのは意外だった。なぜなんだろう。

 

3 isInstanceOfは継承関係とクラスの親子関係のどちらを見ているのか?

「クラスXがクラスYの子クラスであること」と「クラスXがクラスYを継承していること」は同値ではないことを確認したところで次の疑問が湧いてきた。

それはisInstanceOfメソッドはクラスの親子関係と継承関係のどちらを判定しているのかということである。

REPLで試してみる。

scala> class Hoge extends AnyRef

defined class Hoge

 

scala> null.isInstanceOf[Hoge]

res15: Boolean = false

nullはNull型であり、Null型はHogeの子クラスだが、Hogeを継承していない。

この時に、null.isInstanceOf[Hoge]がfalseということはisInstanceOfは継承関係を見ているということだ。

 

4 Nothing型とNull型の子クラスは作れるか?

これは上までとは別の疑問。

「すべてのクラスの子クラス」というものに子クラスを作れるだろうか?という疑問である。できるとしたら、以下のようになってクラスの親子関係がループしてしまうだろう。

  1. Nothing型に子クラスが存在するとする。これをSubNothing型と呼ぶ
  2. SubNothing型はAny型の子クラスであるので、「Nothing型はすべてのAny型の子クラスである」という前提からNothing型はSubNothing型の子クラスである。

Null型についても同じである。

試してみる。

scala> class SubNothing extends Nothing

<console>:11: error: illegal inheritance from final class Nothing

       class SubNothing extends Nothing

                                ^

 

scala> class SubNull extends Null

<console>:11: error: illegal inheritance from final class Null

       class SubNull extends Null

                             ^

どっちもfinal classなので子クラスを作れないとのこと、納得。

 

5 Nothing型を作れるか?

これもまた別の疑問。まずは参照先サイトの以下の記述について。

「値が無い」という意味では、Nothingというクラスがある。
こちらは本当に値が無くて、Nothingを返すよう宣言したメソッドでは、値を返す事が出来ない。

つまり、Nothing型に属するインスタンスはないということだろう。

おそらく、Nothing型のコンストラクタが存在しない、もしくは存在しても呼べないということではないだろうか。

試してみる。

scala> classOf[Nothing].getConstructors.foreach(println)

public scala.runtime.Nothing$()

おぉ、Nothing型にコンストラクタは存在するようだ。

scala> classOf[Nothing].getConstructor()

res23: java.lang.reflect.Constructor[Nothing] = public scala.runtime.Nothing$()

しかも、コンストラクタを取得できる。

scala> classOf[Nothing].getConstructor().newInstance()

java.lang.InstantiationException

  at sun.reflect.InstantiationExceptionConstructorAccessorImpl.newInstance(InstantiationExceptionConstructorAccessorImpl.java:48)

  at java.lang.reflect.Constructor.newInstance(Constructor.java:423)

  ... 32 elided

しかし、コンストラクタを実行するとInstantiationExceptionとなってしまった。やはりNothing型のインスタンスは作れないようだ。

 

ちなみにNull型については

scala> classOf[Null].getConstructors.foreach(println)

 

そもそもコンストラクタが無いようだ。

 

まとめ

ScalaのNothing型とNull型について色々と試行錯誤。

Nothing型もNull型もすべての親クラスのメソッド・フィールドを持っているわけでは無い。

「クラスの親子関係」と「クラスの継承関係」はイコールでは無い。

Nothing型はThrowableを継承した、すべてのAny型の子クラス。

Null型はObjectを継承した、すべてのAnyRef型の子クラス。

isInstaceOfは継承関係をチェックする。

Nothing型とNull型は継承できない。

Nothing型はコンストラクタを持つがインスタンスを作ろうとすると例外となる。

 

Nothing型がThrowableな理由は後で調べたいなぁ。

三日坊主かもしれないけれど

こんにちは、ぽりへどろんです。

 

まずはテスト投稿も兼ねて、適当に。

 

普段は魔法厨房というサークルでゲーム制作のためのプログラムを書いているのですが、色々調べたことをそのままにしておくのもな〜、ということでブログに書いてみることにしました。

ツッコミどころのあることを書くかもしれませんが、その時は遠慮なく突っ込んでください。

 

プログラミングは大体Scalaなので、内容もScalaになると思います。

 

あとはアニメ、マンガ、ゲーム系のオタクブログですね。

 

ではでは。