ゲームランチャーの自動読み込み機能を作る

  • 7
    いいね
  • 0
    コメント

 この記事はCCS Advent Calendar 2016の24日目の記事です。

 どうも、まっそうめん(@massoumen)です。2日前にも記事を書いた人です。2日前の記事はCCSの活動と全く関係ありませんが今回はCCSの活動と関係する内容となっています。よろしくお願いします。

はじめに

 僕の所属しているCCSというサークルでは新歓や大学祭で所属会員が作ったゲームを展示しています。なのでゲームで遊ぶ人が遊びやすいようにゲームを選んで起動するためのランチャーを作っています。ちなみに今年の大学祭ではこんな感じのランチャーを作りました。

012.png

014.png

 スクリーンショットを選ぶとそのゲームの操作方法など詳細が表示され、「このゲームで遊ぶ」をクリックするとゲームが起動するようになっています。スクショを見てどのゲームで遊ぶかを決めたり、単純に操作しやすいなど、遊ぶ人の事を考えるならゲーム起動用のランチャーはぜひ用意しておきたいものです。

 しかし一つ問題があって、ランチャーって意外と用意するのが大変というか、作り方が良く分からないんですね。今年の新歓期にいきなり「新歓で展示する用のランチャー作って!」って投げられたんですが作り方が全く分からなくて正直かなり困りました。ネットで調べても全然見つかりませんし。たまたまもっと上の代の先輩がその様子を見かけて教えてくれたので作れたのですが、それが無かったら作れませんでしたね…(過去のランチャーのデータ探すとか、自分で聞きに行けば良かったのではって感じが今となってはするんですが)

 そんなこんなで今年の新歓と大学祭では僕がランチャーを作りました。なんだかんだ楽しかったのですが、意外と疲れたので今後は後輩に作らせる作ってほしいなって。プログラム制作に興味がある人にとってはランチャー制作はいい機会なのでぜひやってみてほしいなとも思います。

 しかし最低限の道のりが分からないとやりにくいものです。
 そんな訳で!僕なりのですがランチャーの作り方を記そうかなと。前置きが長いわ

この記事の流れ

  • 使用する言語とライブラリ
  • Siv3Dについて
  • 自動読み込み機能について
  • 自動読み込み機能の実装(AppLoaderクラス)
  • 自動読み込み機能の使用(Launcherクラス)

使用する言語やライブラリ

 僕はこれまで以下の2パターンでランチャーを作りました。

  • C#とDXライブラリ
  • C++とSiv3D

 「C#とDXライブラリ」ではC#の機能で他のソフトウェア(ゲーム)の起動を行い、画像の表示などビジュアル面に関しては使い慣れてるDXライブラリを使った感じです。(先輩から教わったのはこの方法で、新歓のランチャーはこの組み合わせで作りました)

 「C++とSiv3D」では単にC++が好きでゲーム制作でもよく使っている(その頃の僕はC++とDXライブラリでゲーム制作をしていました)ので、よく分かってないC#ではなくある程度書いたことのあるC++でランチャーを作れないかと探していたらSiv3Dというライブラリに出会い、作ったという感じです。(大学祭のランチャーはこの組み合わせで作りました)

 今回の記事では「C++とSiv3D」でランチャーを作る方法について述べます。これはサークル内でC#をやってる人が恐らく少ない(大体の人がC言語やC++を使っているので)というのと単に僕がC++とSiv3Dが好きだからというのが理由です。(C#とDXライブラリの方もせっかくなので機会を見つけて書きたいですね)

Siv3Dについて

 Siv3DはC++を用いて画像の描画や音の再生などゲーム制作に必須の機能から、当たり判定やシーン遷移などゲーム制作では苦労するところまで簡単に実装できるライブラリです。また、ゲーム制作とは直接関係なさそうな機能もたくさん用意されていて、今回のランチャー制作では指定したディレクトリにあるファイルの一覧を取得する機能などを利用します。

 ここではSiv3Dについて細かい説明は省かせてもらいますが、とにかくすごいのでC++を少し触ったっていう人は是非試してみてください。公式サイトはこちら

自動読み込み機能について

 ランチャーのデザイン(ビジュアル面)に関しては作る人のセンスで、見やすさを重視したり、かわいい系にするとか、かっこいい系にするとかいろいろあると思うのでその辺はひとまず置いといて、自動読み込み機能について触れようと思います。

 自動読み込み機能とは展示のために用意した複数のゲームに対し、それぞれのスクショとか操作説明のテキストなどをランチャーが自動で読み込んでくれる機能の事です。(名前は僕が適当に付けただけなのであまり気にしないでください)

 なぜそのような事をするのかというと、それをやらないとゲームの数が増えた時にいちいちプログラムの中身を書き換えないといけなくなるからです。

 Siv3Dでランチャーを作る記事を初めて読んだとき、Siv3Dでは100行も書かずにここまで綺麗なランチャーが作れるとわかって「すごい!」って気持ちになった(ぜひ読んで試してみてください)のですが、このプログラムでは起動したいソフトウェアが増えた時にいちいち.exeのパスや表示する名前を追加しなければいけません。

    const Array<Application> applications =
    {
        { L"メモ帳", L"C:/Windows/System32/notepad.exe", L"", L"メモを書けるよ", texture },
        { L"ペイント", L"C:/Windows/System32/mspaint.exe", L"", L"絵を描けるよ", texture },
        { L"電卓", L"C:/Windows/System32/calc.exe", L"", L"計算してくれるよ", texture },

        //ここに追加しなければいけない
    };

 CCSでは会員が各自で作りたいようにゲームを作っているので展示する日の直前までどれくらいの数のゲームが集まるかが不明となっています。(他のサークルではその辺どんな感じなんでしょうか。気になるから誰か教えて下さい)

 もっと言えばCCSの会員限定なのか世の大学生がそうなのかは分かりませんが、締め切りまでに余裕をもって完成させるのではなく締め切り直前まで制作を続けているチキンレースので、ゲームが増えるたびにプログラムを修正するのは微妙な感じです(別にそれでもいいのですが)。なので、追加したゲームを自動で読み込めるようなランチャーをあらかじめ作っておきましょう。

自動読み込み機能の実装

 ここからは実際にソースコードを載せながら解説していきます。C++とSiv3Dを利用するので実際に動かす場合は環境を導入して実行して下さい。また、C++の基礎的な書き方はわかってる前提で話すのでお許しください。

1. 読み込むためのルールを決める

 自動でゲームを読み込むと言っても何かしらのルールを定めておかないとプログラムを作れないので、今回は次のようなルールを定めました。

 あらかじめ作成したGameフォルダ内に作ったゲームの入ったファイルを入れると、

  • ファイルの名前をゲームの名前として読み込む
  • ゲームの実行ファイル「.exe」のパスを読み込む
  • ファイルの中にある「screenshot」という名前の画像ファイルを読み込む
  • ファイルの中にある「readme.txt」というテキストファイルのテキストを読み込む
  • ファイルの中にある「info.txt」というテキストファイルのテキストの1行目と2行目を読み込む

が自動で行われるようにします。「.exe」のパスはランチャーからゲームを起動するために、「screenshot」という名前の画像ファイルはゲームのスクショを表示するために、「readme.txt」はゲームの説明を表示するために、「info.txt」は1行目にゲームのジャンルを、2行目に操作に使うもの(キーボードやマウス)を表示するために読み込みます。

 分かりにくいと思うので一応ファイルの配置を画像でも。まず、Gameフォルダを.slnファイル(画像の場合FestivalLauncher.2016.sln)とかがある場所に作ってください。

007.png

 このGameフォルダの中に作成したゲームの入ったフォルダをゲーム制作者が入れていきます。試しに僕が過去に作ったゲームを入れた結果、Gameフォルダの中身はこんな感じになりました。

010.png

 このそれぞれのフォルダの中に制作したゲームの実行ファイルやreadme.txtなんかが入ってます。試しに「ぜんら」の中身を見てみましょう。名前は気にしたら負け

011.png

 FullFrontal.exeが実行ファイルです。これを実行すれば当然このゲームが起動でき、ランチャーからこれを実行するのが目標です。また、screenshot.pngはこのゲームのスクショ、readme.txtはルール説明や操作方法が、info.txtはランチャー上で表示する用のゲームのジャンルや操作に使うものが書いてあります。それぞれ中身はこんな感じです。

(screenshot.png)
screenshot.png

readme.txt
<操作方法>
スペースキー:押し続けている間、服を着ます。

<概要>
・ともだちがこっちを向いている間は服を着ましょう。完全にこっちを向くまでは
セーフです。ギリギリを追求してもいいでしょう。
・1分間服を脱いだ状態が見つからなければゲームクリアです。

<お借りした素材>
(効果音・BGM)
サイト名:フリー音楽素材/魔王魂
URL:http://maoudamashii.jokersounds.com/
info.txt
全裸ゲー
キーボード

 このようにファイルを配置したときに、ゲーム名としてファイル名の「ぜんら」、ゲームを起動するための.exeのパス、screenshot.png、readme.txtの中身、info.txtの1行目と2行目を自動で読み込むようにする(他のフォルダたちも同様)というのが自動読み込み機能の目標となります。

2. AppLoaderクラスの作成

 では、これらを読み込むためのAppLoaderクラスというものを早速作ってみましょう。(名前はお好きにどうぞ)

 AppLoader.hというヘッダファイルにAppLoaderクラスの宣言を、AppLoader.cppというソースファイルに実装を書いていきたいと思います。また、データをまとめるための構造体をいくつか用意したのでそれをDefine.hというヘッダファイルに書いておきます。まずはヘッダファイルから。

Define.h
#pragma once
#include <Siv3d.hpp>

//info.txtを読み込んで保存する用
struct Info {
    String kind;
    String usingtext;
};

//.exeのパスやスクショなど読み込んだものをまとめて保存する用
struct Application {
    unsigned int id;
    String name;
    FilePath exepath;
    Texture screenshot;
    Info info;
    String readme;
};
AppLoader.h
#pragma once
#include <Siv3D.hpp>
#include "Define.h"

class AppLoader {
private:

    //読み込む対象のフォルダ名(今回は./Game)
    FilePath folderpath;

    //引数のパスが示すフォルダ内の.exeを見つけてそのパスを返す
    FilePath SearchExePath(const FilePath& path);

    //引数のパスが示すフォルダ内のscreenshotを見つけてTextureとして読み込み返す
    Texture SearchScreenShot(const FilePath& path);

    //引数のパスが示すフォルダ内のinfo.txtを見つけて1,2行目を読み込みInfo型(自作した構造体)として返す
    Info SearchInfo(const FilePath& path);

    //引数のパスが示すフォルダ内のreadme.txtを見つけて中身を全部読み込んで返す
    String SearchReadMe(const FilePath& paht);

public:

    //読み込む対象のフォルダのパスを引数としたコンストラクタ
    AppLoader(const FilePath& path);

    //ゲームやスクショなどのデータを全部読み込んでApplication(自作の構造体)の可変長配列として返す
    Array<Application> Load();
};

 Siv3Dで独自に用意されている型がたくさんあるので軽く紹介しておきます。

  • String : Siv3Dで用意されている文字列用の型
  • Array : Siv3Dで用意されている可変長配列
  • FilePath : ファイルのパスを保存するための型。名前が違うだけでStringと一緒。単に分かりやすくしているだけ?
  • Texture : Siv3Dで用意されている画像を表示したりするのに使う型

 ソースファイルはこんな感じです。

AppLoader.cpp
#include "AppLoader.h"

AppLoader::AppLoader(const FilePath& path) :folderpath(path)
{

}

FilePath AppLoader::SearchExePath(const FilePath& path)
{
    Array<FilePath> files = FileSystem::DirectoryContents(path);
    FilePath exepath;

    for (const auto& file : files) {
        if (file.includes(L".exe") == true) {
            exepath = file;
            break;
        }
    }

    return exepath;
}

Texture AppLoader::SearchScreenShot(const FilePath& path)
{
    Array<FilePath> files = FileSystem::DirectoryContents(path);
    FilePath sspath;

    for (const auto& file : files) {
        if (file.includes(L"screenshot") == true) {
            sspath = file;
            break;
        }
    }

    Texture texture(sspath);

    return texture;
}

Info AppLoader::SearchInfo(const FilePath& path)
{
    Array<FilePath> files = FileSystem::DirectoryContents(path);
    Info info;
    TextReader reader;

    for (const auto& file : files) {
        if (file.includes(L"info.txt") == true) {
            reader.open(file);
            reader.readLine(info.kind);
            reader.readLine(info.usingtext);
            break;
        }
    }

    return info;
}

String AppLoader::SearchReadMe(const FilePath& path)
{
    Array<FilePath> files = FileSystem::DirectoryContents(path);
    String readme;
    TextReader reader;

    for (const auto& file : files) {
        if (file.includes(L"readme.txt") == true) {
            reader.open(file);
            readme = reader.readAll();
            break;
        }
    }

    return readme;
}

Array<Application> AppLoader::Load()
{
    Array<FilePath> folders = FileSystem::DirectoryContents(folderpath);//Gameフォルダ内のフォルダの絶対パスが代入される(「C:Users/(なんたらかんたら)/game/旅するナイト/」の形)

    for (unsigned int i = 0; i < folders.size(); i++) {
        Application temp = { i,FileSystem::BaseName(folders[i]),SearchExePath(folders[i]),SearchScreenShot(folders[i]),SearchInfo(folders[i]),SearchReadMe(folders[i]) };//「なんちゃら/game/ぜんら」でも「なんちゃら/game/ぜんら/」でも挙動は同じ?
        applications.push_back(temp);
    }

    return applications;
}

 FileSystem::DirectoryContents("パス名")はSiv3Dで用意された関数で、引数に渡したパスの示すフォルダの中のファイルやフォルダのパスを全部まとめてFilePathの可変長配列(Array<FilePath>)として返します。Load関数ではまずGameフォルダ内(正確にはAppLoaderクラスのオブジェクトを生成する時にコンストラクタに引数として渡したパスのフォルダ内)のフォルダのパスをfoldersに代入します。例えばGameフォルダの中身が少し前の画像のようになっていた場合は

  • folders[0] = "C:Users/(なんたらかんたら)/Game/FullFrontalRevolution/"
  • folders[1] = "C:Users/(なんたらかんたら)/Game/あおてんじょう/"
  • folders[2] = "C:Users/(なんたらかんたら)/Game/エンシェントジャガイモ/"
  • folders[3] = "C:Users/(なんたらかんたら)/Game/ぜんら/"
  • folders[4] = "C:Users/(なんたらかんたら)/Game/旅するナイト/"

といった感じになります。(順番がこの通りかどうかは覚えていませんが…)

 Gameフォルダ内の読み込みが終わったら次にGameフォルダ内の各フォルダに対してあれこれ読み込み、あらかじめ作っておいた自作の構造体であるApplication型にまとめて、Application型の可変長配列として返します。

 .exeのパスやスクリーンショットはSearch○○という関数を作り、Gameフォルダ内の各フォルダのパスを与えることでそのパスの中の該当するファイルを探したり中身の文章を読み込んだりして、それを返すようにして作っています。例えば上のようにfoldersにパスが代入された場合のfor文のループ変数i = 3、すなわちfolder[3]のときの挙動について考えます。

 まず、temp.idにはiが代入されるのでtemp.id = 3となります。

 次にtemp.nameに代入するものとしてFileSystem::BaseName(folders[i])と書いています。これはSiv3Dで用意された関数で、パスを引数に与えると、その最後の部分(スラッシュ抜き)だけを返してくれるというものです。今回はfolders[3]の場合を考えているので"C:Users/(なんたらかんたら)/Game/ぜんら/"のうちの"ぜんら"の部分が返ってきて、それがtemp.nameに代入されます。結果、temp.name = "ぜんら"となります。

 次はtemp.exepathです。SearchExePath関数は今回自分で用意したもので、その中身を見てみると、まずFileSystem::DirectoryContens("パス名")を利用して引数として受け取ったパス、今回の場合は「ぜんら」フォルダの中の"picture", "sound", "FullFrontal.exe", "info.txt", "Log.txt", "readme.txt", "screenshot.png"のパスがArray<FilePath>としてfilesに代入されます。その後、for文を用いてfilesの中身に.exeがあるかを探します。探すためにString(FilePath)型のメンバ関数であるinclude("調べたい文字列")を利用します。この関数の引数に与えた文字列がその変数に含まれていたらtrueを、そうでなければfalseを返してくるのでそれで判断します。

 少し話が逸れますが、for文のカッコの中が意味不明だと感じる人がいるかもしれません。これは「range-based for文」というもので、C++11以降では配列とかの要素を最初から最後まで調べるときにこうやって書くことができます。よく分からんぞって人はこれらと同じだと思って下さい。

for (unsigned int i = 0; i < files.size(); i++) {
    if (files[i].includes(L".exe") == true) {
        exepath = files[i];
        break;
    }
}
for (auto iter = files.begin(); iter != files.end(); iter++) {
    if (iter->includes(L".exe") == true) {
        exepath = *iter;
        break;
    }
}

 話を戻すと、".exe"が含まれていたらそいつが実行ファイルなので(そうであってほしい)、それをexepathとして返します。結局、folder[3]を引数として渡すとSearchExePath関数は"C:Users/(なんたらかんたら)/Game/ぜんら/FullFrontal.exe/"を返すのでtemp.exepath = "C:Users/(なんたらかんたら)/Game/ぜんら/FullFrontal.exe/"となります。

 次はtemp.screenshotですが、これはSearchScreenShot関数の返り値が代入されます。SearchScreenShot関数内でやっていることはSearchExePath関数内でやっていることとほぼ同じなので省略しますが、folder[3]を引数に与える場合はtemp.textureには「ぜんら」フォルダ内の
screenshot.pngがTextureとして読み込まれ、それが返ってきます。

 次はtemp.infoです。Info型は2つの文字列からなり、片方がゲームジャンル、もう片方が使用するものを示します。SearchInto関数ではSearchExePath関数やSearchScreenShot関数と同様にまず"info.txt"を見つけます。その後Siv3Dの機能であるTexReaderを利用し1行目と2行目を読み込みます。folder[3]を引数として与えた場合、temp.info.kind = "全裸ゲー", temp.info.usingtext = "キーボード"となります。

 最後にtemp.readmeについて。これはtemp.infoとほぼ同じで"readme.txt"を見つけてその中身を全部保存します。readLine()は1行ずつ読むのに対しreadAll()は全部読み込むみたいです。

 こんな感じでtempに代入しては可変長配列applicationsにまとめ、最後にそれを返します。これで無事Gameフォルダにあるすべてのゲームの実行ファイルのパスやスクショ、その他必要と思われる情報が読み込めました。もし他に読み込みたいデータ(例えば制作者の名前など)がある場合はここら辺を編集して実装して下さい。

 なんとか自動読み込み機能を作りましたが実は問題があって、例えばscreenshot.exeっていう名前のファイルとかあったらどうなってしまうんだろう…って。しょうがないので今回はとりあえずスルーします。誰かいい感じの方法があったらぜひ教えてください。

 また、.exeやreadme.txtが無い場合などの処理も用意していません。そこら辺の実装もできるとより良いランチャーになるでしょう。(僕は時間が無くて断念しましたorz)

自動読み込み機能の使用

 なんとかAppLoaderクラスを作り終えたのでそれを実際に使用しましょう。Launcherクラスという実際にランチャーの描画とか操作とかの処理を行うクラスを作り、その中で使用することにします。Launcher.hというヘッダファイルに宣言部を、Launcher.cppというソースファイルに実装部を書きます。

Launcher.h
class Launcher {
private:
    Array<Application> applications;
    AppLoader apploader;
    Optional<ProcessInfo> process;

    //描画などに必要な変数や関数(ここでは省略)

public:
    Launcher(const FilePath& path);

    void update();//入力などの処理
    void draw() const;//描画処理
};
Launcher.cpp
#include "Launcher.h"

Launcher::Launcher(const FilePath& path) :apploader(path)
{
    applications = apploader.Load();

    //描画などに必要な変数や関数(ここでは省略)
}

void Launcher::update()
{
    //ゲームが起動中(process!=none)だとtrue、そうでない(process==none)場合falseとなる
    if (process) {
        if (process->isRunning()) {
            Window::Minimize();//ゲーム起動中はランチャーを最小化
        }
        else {
            Window::Restore();//ゲームが終了したらランチャーの最小化を解除
            process = none;//ゲームが起動していない状態にセット
        }
    }
    else {
        //ゲームが起動していない間のランチャーの操作処理(ここでは省略)

        //ゲームを起動するタイミングで以下の関数を実行
        process = System::CreateProcess(applications[起動したいゲームのインデックス].exepath);
    }
}

void Launcher::draw() const
{
    //ランチャーの描画いろいろ(ここでは省略)
}

 LauncherクラスのコンストラクタでAppLoaderのLoad関数を利用してApplicationの可変長配列applicationsにゲーム情報を読み込んでいます。スクショの描画やreadmeの情報を表示したい場合はapplicationsの各要素を参照すればokです。

 また、Siv3Dで独自に用意されている型が新たに出てきたので少し紹介します。

  • ProcessInfo : プロセスに関する情報を持つ
  • Optional : <>で挟まれた型に無効値(none)の状態を追加する(noneでない場合はその型に対応する何らかの値を通常通り持つ)

 また、System::CreateProcess("起動したいアプリのパス")はSiv3Dで用意された関数で引数に与えたパスのアプリを起動してくれます。また、ProcessInfo型の返り値を返すので(中身はよくわかりませんが)processに代入します。すると、processの値はnoneではなくなるのでゲームを起動するとランチャーは最小化されるようになり、ゲームプレイの邪魔をしません。ゲームが終了するとそのタイミングでprocessにnoneを代入しランチャーの最小化を解除するので、再びランチャーを操作することができます。

 今回は時間やスペースの都合上省略しましたが、描画部分やランチャーの操作部分を実装すればLauncherクラスが完成します。あとはMain.cppでLaucherクラスのオブジェクトを生成(Gameフォルダのパスをコンストラクタの引数に与える)してループ内でLauncherクラスのupdate関数とdraw関数を実行すればランチャーが完成します。

main.cpp
#include <Siv3D.hpp>
#include "Launcher.h"

void Main()
{
    //ウインドウの大きさ設定や描画に必要な画像ファイルの読み込み(ここでは省略)

    Launcher launcher(L"./Game");

    while (System::Update())
    {
        launcher.update();
        launcher.draw();
    }
}

おわりに

 自動読み込み機能の話だけして描画部分については一切触れていません。ごめんなさい。大学祭で作ったランチャーは描画部分がとんでもなく雑に書かれており、とてもじゃないですけど何が何だか分からない感じになってしまっているので思い切ってカットしました。

 ゲームランチャー制作、ぜひ挑戦してみて下さい。最後まで読んでいただきありがとうございました。

 明日の記事はとっちーさん(@yoooomaruuuu)です。よろしくお願いします。