チャチャチャおもちゃの抹茶っちゃ

ゲームのこととかプログラミングのこととか。気が向いたら書く。ブログタイトルは友人が考えました。

【C++,Siv3D】ゲームランチャーを制作する

はじめに

サークル活動のひとつとして、展示ゲーム用のランチャーをC++とSiv3Dを使って作った話。

うちのサークルでは会員がゲームを作ったりしていて、新歓だとか大祭のときに制作物の展示を行っている。ただデスクトップから起動するだけでは寂しいので毎度ランチャーを用意することにしている。今回はそのランチャーをぼくが作ったので備忘録。

作り方とかの大部分はググって出てくる通りなので、ここでは詰まった点とか気をつけた点とかを書いておく。

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

  • C++
  • Siv3D(August 2016 v2)

使用言語はC++C#とかもあるみたいだけど触ったことがない。最近C++に結構触っていて勉強もしていたのでその流れでC++で作ることにした。というかものを作れるほど使える言語そんな多くないよね

使用するライブラリはSiv3D。

play-siv3d.hateblo.jp

当たり判定とかも関数で用意されていてかなり使いやすそうで前々から興味はあった。せっかくだからSiv3D触ってみながらランチャーつくるかなーという次第。実際使ってみるとかなり小回り効いていい感じだった。

せっかくSiv3Dを使うので、便利な当たり判定からシーン遷移まで使わせてもらいました。マジ便利。

以前DxLibの紹介記事みたいの書いておきながらあれだけど、Siv3D欲しいものがすでに揃ってるの便利すぎて戻れなくなりそう()
自作ライブラリとかゴミ!wってなる

目標

ランチャーを作る。自動読み込み機能付き。UIが説明されなくても直感的にわかるとなお良い。

技術的な話

ランチャーなので他アプリケーションを起動できないとはじまりませんね。あとはプログラムのほうで毎回起動するプログラムを追加していたらキリがないので、ディレクトリの状態からアプリケーションを自動取得する機能も欲しいです。

とりあえずググりました。「C++ ランチャー」。Qiitaの記事がヒット。あれ・・・・・・?著者がサークルの先輩ですね・・・・・・。

qiita.com

ここを見るとSiv3Dのほうの関数でディレクトリの状態とかを取得できるものがある様子。ほぼ丸パクリします。

自動読み込み機能

上記サイトのを参考にしつつ改良を加えていく。詳しい解説はここでは書きません。ほぼ同じことします。

読み込みルール

とりあえず引用。

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

ここからいくつか変更を加えていく。

まず強調した部分。実行ファイルがEXE形式とは限らなかったり複数ある可能性を加味して自分で実行ファイルを指定できるように。info.txtの1行目に実行ファイル名を記述するようにして、2行目からゲームジャンルなどを読み込むように仕様を修正。

スクリーンショット以外にデモムービーもロードして流すことができるようにしたいので、「demo」のつく動画ファイルも読み込むようにした。

基本的には引用ママなので、一部変えたところだけ。

// 略

// 実行ファイル名、ジャンル、操作方法のロード
if ((file.includes(L"info")) {
    TextReader reader(file);
    infoFlag = true;
    reader.readLine(appData.executePath);
    reader.readLine(appData.kind);
    reader.readLine(appData.usingtext);
    continue;
}

// 実行ファイルのパス変換
appData.executePath = path + appData.executePath;

// 略

ディレクトリのパスとinfo.txtから読み取った実行ファイル名を連結してあげてパスが完成。Siv3DのFilePath型はString型の別名でしかないので同じように扱うことができる。

あとはエラー処理とかをよしなに書き込んであげるとより良いでしょう。とりあえずぼくはうまく読み込めなかったらPrintln()デバッグ表示するようにした。

Siv3DのFileSystem::にある関数たち、なかなか使いやすいですねえ・・・・・・。

表示の都合の読み込み方変更

readme表示の都合上テクスチャにすると楽だったので変換した。以下変換コード。

// readmeのロード
if (file.includes(L"readme") || file.includes(L"readMe") || file.includes(L"Readme") || file.includes(L"ReadMe") || file.includes(L"README")) {
    // テキストファイル読み込み
    TextReader reader(file);
    String content = reader.readAll();
    readmeFlag = true;

    // イメージ変換
    Image contentImg;
    contentImg.resize(font.region(content).size, Palette::White);
    font.write(contentImg, content, Point::Zero, Palette::Black);
    appData.readme = Texture(contentImg);

    continue;
}

ところでString型で正規表現とかどう使うんでしょうね(使い方わからなかったけど調べるほどでもなかった気がして後回しにしていた)。

テクスチャ変換をすると

github.com

これが使えるので脳死でReadmeをきれいに貼り付けることができる。というかテキストをフィールド作って貼り付ける方法がうまく見つからなかったので・・・・・・。

ランチャー機能

Issue見てもわかってしまうけれど一番悩んだところ。EXE形式のファイルを開くだけなら特に問題なかった。 Siv3DのCreateProcess()ProcessInfo型の2つを使えば比較的容易に起動やランチャーの最大化、最小化ができた。参考にしたのはこれ↓

qiita.com

ちょうどよいサンプルがあったね。

問題となったのはこのあと。EXE形式以外のゲームもサークルでは作る人がいるかもしれない(HTMLゲーなど)ので、EXE形式以外のファイルも起動したい・・・・・・。CreateProcess()ProcessInfo型で管理するのはできればそのままにしたいという方向で調査を進める。

どうやら既定のプログラムで開きたいときにそのプログラムを取得する方法としてWinAPIのFindExecutable()というものがあるらしい。こちらを利用してブラウザのパスを取得してCreateProcess()にわたすことを試みる。

無事既定プログラムのパスは取得できたものの、ブラウザによってHTMLファイルを開くときのコマンドラインオプションが異なったり、そもそもCreateProcess()がうまく動かなかったりして詰まる。手詰まりになったので掲示板に質問してみた。↓

siv3d.jp

Siv3D作者のReputelessさんにもお手数をおかけして教えていただいた。Chromeで試せなかったのはぼくがガバってたらしい?恥ずかしい・・・・・・。ここでShellExecuteW()を使う手を得る。しかしこのままでは起動したプロセスが終了したかどうかとか起動中かどうかを知ることができない・・・・・・。ShellExecuteまわりで調べだすと、プロセスをハンドルできそうなShellExecuteEx()なるものがあるようなのでいじりだす。新たにプロセスを起動してそのハンドルを返す関数を実装して動かしてみるとうまく動くようだった。ハンドルのクローズをしないとメモリリークを起こすようで、この辺の実装にも気を配る必要がありそう。慣習的にはShellExecuteEx()を呼んだらすぐクローズするのがお作法なようだけどこのハンドルを使いたいのでやむを得ず。試しに次のようなコードを書いてみるとうまく外部アプリケーションを起動でき、ランチャーの最小化と復帰もできた。

# include <Siv3D.hpp>
# define  NOMINMAX
# include <Windows.h>
# include <psapi.h>

HANDLE OpenApps(const FilePath& url, const FilePath& dir)
{

    Println(FileSystem::FileName(url));
    Println(FileSystem::ParentPath(url));

    SHELLEXECUTEINFOW shellExeInfo;
    memset(&shellExeInfo, 0, sizeof(shellExeInfo));
    shellExeInfo.cbSize = sizeof(shellExeInfo);
    shellExeInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
    shellExeInfo.lpVerb = L"open";
    shellExeInfo.lpFile = url.c_str();
    shellExeInfo.lpParameters = nullptr;
    shellExeInfo.lpDirectory = dir.c_str();
    shellExeInfo.nShow = SW_SHOWNORMAL;

    ShellExecuteEx(&shellExeInfo);
    HANDLE handle = shellExeInfo.hProcess;

    return handle;
}

void Main()
{
    HANDLE handle = 0;

    const FilePath filePath = L"testapp.exe";

    while (System::Update())
    {
        if (handle == 0 || WaitForSingleObject(handle, 0) == WAIT_OBJECT_0) {
            Println(L"Restore");
            CloseHandle(handle);
            handle = 0;
            Window::Restore();
        }
        else {
            Println(L"Minimize");
            Window::Minimize();
        }
        if (Input::MouseL.clicked) {
            Println(L"clicked");
            Println(handle = OpenApps(FileSystem::FileName(filePath), FileSystem::ParentPath(filePath)));
        }

    }
}

アプリケーションの起動時とそうでないときでランチャーの状態を分けるのは、handleCreateProcess()ProcessInfoのように使うことで実現。これでうまくいったのを参考にしてランチャーのほうにも実装した。
すると次のような実装になった。起動のための関数は詳細画面用クラス内に作成した。

関数部分(Detail.cpp)

# define NOMINMAX

HANDLE Detail::openOtherApps(const FilePath & file, const FilePath & directory) {

    SHELLEXECUTEINFO shellExeInfo;
    memset(&shellExeInfo, 0, sizeof(shellExeInfo));
    shellExeInfo.cbSize = sizeof(shellExeInfo);
    shellExeInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
    shellExeInfo.lpVerb = L"open";
    shellExeInfo.lpFile = file.c_str();
    shellExeInfo.lpParameters = nullptr;
    shellExeInfo.lpDirectory = directory.c_str();
    shellExeInfo.nShow = SW_SHOWNORMAL;

    ShellExecuteExW(&shellExeInfo);

    HANDLE handle = shellExeInfo.hProcess;

    return handle;

}

呼び出し部分(Detail.cpp、updateLauncher関数内)

// あそぶボタンが押されたときの処理
// EXE形式だったらSiv3DのCreateProcessを使う
if (FileSystem::Extension(m_data->apps[m_data->selectedID].appData.executePath) == L"exe") {
    m_data->process = System::CreateProcess(m_data->apps[m_data->selectedID].appData.executePath);
}
// それ以外のときはShellExecuteを採用
else {
    m_data->handle = openOtherApps(FileSystem::FileName(m_data->apps[m_data->selectedID].appData.executePath), FileSystem::ParentPath(m_data->apps[m_data->selectedID].appData.executePath));
}

本当はShellExecuteEx()のみで全部対応できているのだけど、ShellExecute調査不足のところがあるのでとりあえずSiv3DのCreateProcess()は残した。起動ボタン処理の部分に書いてあげましょう。

ランチャーの最小化、復元部分(BaseScene.cpp)

後述するがランチャー本体の最小化、復元チェックはシーン遷移クラスの親クラスに書いておいている。

# pragma once
# include <Siv3D.hpp>
# include <Windows.h>
# include "Scene.hpp"

# define NOMINMAX

void BaseScene::update() {

    // 起動中のアプリケーションがあれば最小化、終了したら最小化解除
    if (m_data->process) {
        if (m_data->process->isRunning()) {
            Window::Minimize();
        }
        else {
            Window::Restore();
            m_data->process = none;
        }
    }
    else if (m_data->handle) {
        if (m_data->handle == 0 || WaitForSingleObject(m_data->handle, 0) == WAIT_OBJECT_0) {
            CloseHandle(m_data->handle); // 使用のおわったハンドルはクローズ
            m_data->handle = 0;
            Window::Restore();
        }
        else {
            Window::Minimize();
        }
    }
        // 起動中のアプリがなければ情報更新
    else {
        updateLauncher();
    }

}

後々では前半のOptional<ProcessInfo>型processで条件分岐してる部分は消せるかもしれない。これでメイン処理部はこの関数を継承したクラスのupdateLauncher()をオーバーライドして書くだけで良くなった。

シーン遷移

ランチャーでは次の3つのシーンを作った。

  • 選択画面
  • 詳細画面
  • デモ画面

選択画面はアイコンからゲームを選ぶ、詳細画面はゲームのReadmeを表示して開始ボタンがある、デモ画面はデモムービーなどをフルスクリーンで流したりしておく。

シーン遷移ではSiv3DのHamFrameworkを利用させてもらった。フェード処理も少しいじったけれどこちらはサンプルを参考に。↓

github.com

アプリケーションの名前とか実行ファイルのパス、スクリーンショットなどの情報はShareDataとしておいた。

すべてのシーンで共通してアプリケーションの起動チェックを行うので、シーン遷移用クラスを継承した抽象クラス(BaseScene)をつくった。フェード処理も一致しているのでこちらに書いている。update()関数の内容は上に書いたとおりである。クラス定義は次のような感じ。

using SceneChanger = SceneManager<String, ShereData>;

class BaseScene : public SceneChanger::Scene{

public:

    BaseScene() = default;

    virtual ~BaseScene() = default;

    void update() override;

    virtual void updateLauncher() = 0;

    void drawFadeIn(double t) const override {
        draw();
        FadeIn(t);
    }

    void drawFadeOut(double t) const override {
        draw();
        FadeOut(t);
    }

};

FadeIn()FadeOut()は別のところに定義してある。

あとはこのクラスを継承したSelectクラス、Detailクラス、Demoクラスを作成してシーンマネージャーにadd()すればOK。

全シーンで共通した処理があるときの小技だった。

グラフィック面

このへんは作るごとに変わったりするしあまり詳しく書くこともないので概要程度に・・・・・・。基本的には読み込んだ内容をそのままはっつけていけば良い。読み込み面と起動面だけクリアしてしまえばランチャーの制作は勝ったようなもの。

サンプルにあった付箋とか写真のフレームとか参考にしまくった、というか割とパクった

まとまったパーツはクラスにしてしまうと管理しやすいですね(ボタンとか付箋とか写真のクラスを作ってひたすら貼っつけただけの顔)

選択画面

スクロールはボタンをつけてできるだけ視覚的に。とにかくわかりやすく。

f:id:mattyan1053:20190322011233p:plain
去年の冬コミのものをランチャーにいれてみた図

詳細画面

Readmeとかの情報を表示。ムービーがあればここで流れるように。

f:id:mattyan1053:20190322015243p:plain
選択画面、ぼくが少し手伝ったゲーム

デモ画面

スクリーンセーバーみたいな感じで。2分間操作しないと自動的にデモ画面に遷移する。読み込んでおいたムービーを流したりスクリーンショットが流れたり。ここ、縦横比固定に気を配った。

デモ画面のゲーム間遷移でもシーン遷移を使っていて、シーン遷移の入れ子になっている。これはサンプルがあったので参考にした。↓

qiita.com

マウスを動かすと元の画面に戻る。Mouse::Delta()とかいうとても便利なものがあったので実装もとても容易だった。

全体的な話

文字数オーバーではみ出るとかそのへんは適当に対処した。↓をやばそうな文字列に適用しただけ。付箋とかは常にこれ適用してる。

// 文字列を幅wに収まるようにカット
String MakeTailCutString(String str, int32 w, String fontAsset) {

    String retStr = str;

    const size_t n = Max<size_t>(FontAsset(fontAsset).drawableCharacters(retStr, w), 1);
    const bool overflow = (n < str.length);

    if (overflow) {
        retStr = retStr.substr(0, n - 3);
        retStr += L"...";
    }

    return retStr;

}

最後に

以上、自分がランチャーを作ってみて躓いた点とか覚えておきたい点だった。ソースコードを全部のせていてはキリがないので切り貼りだったけどわかりにくくなってしまった・・・・・・。起動直後に呼ぶ関数だったり起動中の処理部だったりに書けばいいだけなので、完全コピペとかでやろうとしなければ困らないと思いたい。C++でランチャーを作る系の記事、ネット上に全然ないので残しておこうかと思った(探しまくった)。

実際のソースコードがみたいとかランチャーほしいって人はコチラ↓

github.com

Githubに置いとくので煮るなり焼くなりなんなりと。Issueも暇があれば見ます。タイマー機能とかそのうちつけてみると便利かなと思っている。

【富士旅行記】高校の同窓と12人という大人数で旅行した話

高校の同窓12人で富士のほうへ旅行へ行った話

春休みなのをいいことに、高校の同窓(よくツイッターにいる人達)で富士五湖のほうへ旅行にいった。 写真も結構とったしせっかくなので備忘録。

12人って意味不明では?

日程

2019年3月8日~11日。3泊4日。

1泊目は富士急のホテル、2,3泊目は12人用のコテージを借りた。やすい。

移動手段

車3台。ぼくともうひとり友人が自分で車を出して、もう一台がレンタカー。12人で割るとかなり安上がりになりますね。

レンタカーは3泊4日で45000弱ぽい。車出す人以外の10人で割ったら安いものだね。相場より高めなのは保険モリモリなせい。ここはお金を出すべきところ。

3台全てに初心者マークついてる。勇気ありすぎる。謎の行動力を発揮した

一日目

基本的には移動する日。少し寄り道もする。最終目的地は富士急についてるホテル。

集合まで

平日駐車場無料の南船橋ららぽに集合することにした。9:30集合。

他2台は無事間に合ったようだけど、こちらは渋滞にひっかかりまくって現地まで二時間弱。なんか途中音のなってる救急車4台見たんですけど物騒すぎない?10:00過ぎたあたりで到着、合流。

f:id:mattyan1053:20190321124924j:plain
集合した車三台

なぜかレンタカーともうひとりウィングロードなんだよな。ワイのスパイクが浮いてる。

ところで別の車で危ない運転してたやついたってマジ?w

お昼休憩(海老名SA)

10:30頃ららぽを出発。京葉道路→首都高→東名ルート。12:30くらいに足柄SAでご飯かなーとか考えていた。

実際のところは首都高でずっと渋滞。抜けるのにかなり時間かかった。東名も序盤少し渋滞(町田らへん?)東名入れたところですでに12:00くらいだったので、海老名で休憩という案が出る。採用してお昼ご飯休憩のため海老名SAへ。12:40頃着。

レンタカーじゃないほうはあまり問題ない(日頃も運転してるからかな)感じだったけどレンタカー勢はなんかてこづってた感。少しSAついてからレクチャー。

お昼休憩では富士宮やきそばを食べた(これは現地で食べる予定がなかったので)。ちょっと冷めてたけど美味しかったです。

f:id:mattyan1053:20190321125316j:plain
海老名SAにて

三島スカイウォーク

東名→小田原厚木道路→箱根峠→三島スカイウォーク。所要時間は一時間くらいだった。道は混むどころかスッカスカ。とても良い気分で走れた。

特に箱根峠のあたりがめちゃくちゃ楽しい。運転手は調子乗っている。ぐにゃぐにゃするところきれいに走るの楽しいンゴ!w 丁寧に走ればそんなに難しいこともない。前とかには箱根バスみたいな運転のプロもいるし安心。なんかカーブすぎるたびに変な声出すやつが後部座席にいた。うるせえ。

f:id:mattyan1053:20190321130154p:plain
箱根峠

正直運転なれてないやつ連れて走る道じゃなかったことは認めるすまんかった

この日は快晴で、とても見晴らしがよかった。最の高。

f:id:mattyan1053:20190321130428j:plain
富士山

f:id:mattyan1053:20190321130455j:plain
スカイウォーク

全長400mあるらしい。長い。そこそこ揺れる、楽しい。

アトラクションもあって、ロングジップスライドとかいうのを何人かやった。かなり怖いだろうけど楽しそうだった。

f:id:mattyan1053:20190321130651j:plain
ロングジップスライド

f:id:mattyan1053:20190321130720j:plain
めっちゃ高い。たまに途中で止まる(風とかで)

f:id:mattyan1053:20190321130757j:plain
ひとりだけ着地が下手なやつがいた図

三島スカイウォークの公式HPはこちら↓

mishima-skywalk.jp

ホテルへ

この時点で午後4時くらい。このあと時間に余裕があれば忍野八海にでも寄ってから・・・・・・と思っていたけどなかったのでホテルへ直行。ここからホテルまでは有料道路を使わなかった。国道246号線を北上したあと御殿場のあたりから138号線、139号線を使ってホテルへ。途中138,139のあたりで渋滞してたのでまた峠道で遊びながら迂回した。迂回してないレンタカー勢とあんま到着かわらなかった。楽しかったので良し。

到着は午後6時くらい。もうだいぶ暗くなってきていた。

予約したホテルはこれ↓

www.highlandresort.co.jp

富士急にくっついてるやつ。高いのかと思いきやそうでもない。6人部屋を2つ。朝食ととなりの温泉券と富士急一日券付きで15700円のプラン。予約してくれた人に感謝。

部屋は6人部屋なだけあって広め。スマブラできるね!

f:id:mattyan1053:20190321131842j:plain
ホテルの部屋

ちなみにぼくはここでふすま上部に手をついたときに棘が親指の関節に刺さって血流してた。抜けない。フロントで「とげ抜きありませんか?」って聞いたら爪切り渡された。とれんがな草。お風呂でふやかして頑張って抜くことに。

夕飯はついていないので、近くのファミレス(Big Boy)へ。普通に食べた。ファミレスなので写真もなし。

温泉にもはいった。当然写真はとっていないのだけれど、いろんな種類のお風呂があって面白かった。ぼくは長風呂勢なのでのんびりやっていた。棘は1本抜けた。

なんか無限に水風呂はいってるやついなかったか??きのせいか??

この日は運転7時間くらいしてたのもあって疲れたので夜そんなスマブラやらずに寝た。てかみんな寝た。

二日目

この日は富士急で遊ぶ日。

朝食

富士急に入れるのが8:45なので6:30には起きて7:00くらいから朝食を食べて準備を進めてた。朝食はビュッフェ形式。制覇するくらいのつもりでいろんなものを少しずつとって食べた。ぼくはビュッフェ形式だとこうするタイプ。

f:id:mattyan1053:20190321133232j:plain
朝食はビュッフェ形式

レストラン内から見える景色がとてもよかった。片側は富士急。反対側は富士山。

f:id:mattyan1053:20190321132931j:plain
アトラクションが見える

f:id:mattyan1053:20190321133132j:plain
真正面に富士山がきれいに見える

富士急ハイランド

ぼくは人生ではじめていった。有名ジェットコースター4つ乗れれば良いかな、という感じ。結論から言うと乗れた。

まずはジェットコースター

乗った順番は高飛車→ドドンパ→FUJIYAMA→ええじゃないか。
ぼくは特にジェットコースターが苦手とかもないので、あまり身構えていなかった。とはいえ、普段遊園地にもいかないので、本格的なジェットコースターの経験はほぼ無かった。

一番こわかったのはドドンパ。最初何もないところから180 km/hまで加速するのが呼吸とまった。それ以外は特にどれも大丈夫だったので、どうやらぼくは急降下とか最初から「ここは落下」「ここは加速する」ってわかってるものは大丈夫らしい。機械的に無理やり加速するほうが怖い。

f:id:mattyan1053:20190321134033j:plain
ええじゃないか~ええじゃないか~(耳に残る音楽)

最初にのった高飛車は待ち時間15分ほど。それ以外は2時間以内には乗れていた印象。午後2時くらいには4種全部乗り終えてたと思う。

お昼ご飯

なんかベビースターラーメンとのコラボとかいってよくわからん飯を提供してた。食べた。微妙だった。

f:id:mattyan1053:20190321135015j:plain
ベビースターオムライス?w

おとなしく普通の頼めばよかった感。

その他のアトラクション

時間があまったのでお化け屋敷いこうかとおもったらもういけなかった。仕方ないので他のを探す。

絶望要塞ってのが近くにあったので並ぶ。待ち時間はワードウルフしてた。「サングラス」が少数派で多数派が「ゴーグル」のときあったのだけど、みんなでサングラスの話してた。ぼくは少数派だったので勝った。多数派の全員が自分を少数派だと思い込むミス。

これ謎解き系のやつだったのだけど、全員失敗した。惨敗。なんなら12人中8人は一番最初のミッションで失敗してた。あたまがわるい。てかこれ普通に難しくないか?

コテージへ

午後五時くらいに富士急を出てコテージへ向かう。一台はコテージへ向かって、残り二台は買い出しへ。ここで意思疎通が下手くそすぎて時間がかかる。

最初業務スーパーよったのだけどあまり目当てのものがなくて困った。結局そのあと普通のスーパーにもいって解決。夕飯と翌日の朝食用の食材を購入した。2食分しかかっていないはずなのに、さすがに12人分となるとそれだけでもエグい量になった

コテージに到着して荷物とかを置く。寒いのを除けば割といい感じのコテージだった。

f:id:mattyan1053:20190321135929j:plain
外観。車3台は縦列駐車(奥のは出られない)

f:id:mattyan1053:20190321140229j:plain
コテージ内にロフトもある

借りたコテージはここ↓

uyamaresort.com

一人1泊あたり6500円ちょいくらい。2泊で13500円とかだった。

コテージに全員到着した時点でだいぶ時間がたってた。温泉いこうとしたけど車出すのはめんどいし、あるきだと遠かったので断念。翌日にまわした。

夕食

みんなで餃子パーティー青椒肉絲もついてくる。なんでこれになったかはよくわからん。まあうまかった。てか騒いでるのでだいたい楽しい。

f:id:mattyan1053:20190321140435j:plain
餃子。水を入れすぎた図

ホットプレートは普通に貸出でおいてあった。たすかる。食器類も結構充実してて有能だった。

星をみた

周りに街灯が全然ないので、星空がとてもきれいに見えた。5等星くらいでも普通に見えた。めっちゃきれい。

写真撮ろうとがんばってみたけどあまりうまくいかなかった。設定とかいろいろいじってみたんだけどなあ。それでもまあ満点の星空であることはよくわかった。こんなにきれいに見えると思っていなかったので、三脚をもっていってなかったんだよな、そこが失敗。

f:id:mattyan1053:20190321140745j:plain
星空がよくみえる

f:id:mattyan1053:20190321140841j:plain
スマホでもオリオン座が撮れる

森の中なのでどうしても木がはいってしまうのはご愛嬌。

三日目

のんびりする日。BBQがメイン。

めっちゃ寒くて目が覚めた。和室で7人、ロフトで4人、ダイニングで1人寝てた。ぼくはダイニング。3箇所の中で一番寒かったぽいな。4度。布団が薄い。毛布欲しくなる。

適当にパンとベーコンエッグとコーヒーで優雅に朝食。正直4人くらいしか起きてこないやろ~wwwとか思ってたら11人くらい起きてきた。びっくり。朝食全員分買ってなかったんだよな。

午前中

ぐだぐだと午前中を過ごす。

f:id:mattyan1053:20190321141456j:plain
コテージでスマブラをするひとたち

温泉行く勢とコテージ残る勢に分かれる。ぼくは温泉へ。温泉大好きマン。このときは温泉勢6人かな。

温泉と買い出し

温泉勢は5人を超えたので車は2台出して山中湖反対側の紅富士の湯へ。公式サイトはここ↓

www.benifuji.co.jp

景色が本当によかった。晴れていたしね。風呂も種類があって気持ちが良い。長風呂勢なので1時間強はいってた。あとここで2本目の親指に刺さっていた棘が抜けた。これでかいけつですね!

そのあとBBQ用の買い出しに温泉勢で行く。今回は最初から普通のスーパーへ。食いたいもんをばかすか買っていく。12人いるし適当に買ったとしても余裕なのであまり考えずにいく。やはり大量になって買い物カゴ4つぶんくらいになってた。購入額1万をゆうに超えているけれど、結局12人で割ると千円ちょい。安すぎる。肉を大量に買ってコテージへ戻る。あと4日目の朝食分とかもかった。

バーベキュー

お昼から夕方にかけてのんびりBBQ。なかにはBBQはじめての人もいたらしい。ぼくは火起こしとかやってた。というかみんなあまりやったことないんかな?(よくキャンプとか行く人) ちなみに皮膚が弱いのでこのあと乾燥で死んだ。

薪とか経由せずにいきなり炭に着火するってのと、思いの外BBQグリルが大きかったので火力調整に手こずる。諦めてBBQはじめて見るもあまりに火力が弱くて生焼けになるので、あとから結構調整することになった。ここの生焼け肉が聞いてあとでお腹壊すことになることをこのときの僕はまだ知らない。

f:id:mattyan1053:20190321142459j:plain
グリルがでかい・・・・・・

炭、そんなに早く火がつくものでもないので更に時間たって1時半くらいに開始。岩塩とかも持ってっていたのでうまい。炭火は良い。

夕飯も兼ねちゃうくらいののんびりさで進めていく。BBQはのんびりやるのが一番良い。火の再調整とかも終わるとだいぶいい感じになってくる。

f:id:mattyan1053:20190321142659j:plain
THE BBQって感じ

大変よかったです。

もっかい!温泉

夜です、お風呂の時間です、また別の温泉いきます。このときは9人。車2台。
ここにいきました↓

www.ishiwarinoyu.jp

コテージから一番近かった温泉。ぬるめの檜風呂があって、ここで延々とだべっていた。8人で。一時間くらいだべっていたのかな。一人だけ早めにあがっていたようで、残り8人が延々とだべっていたものだからそのまま1時間待たせてしまった・・・・・・すまねえ・・・・・・。ぼくの長風呂癖につき合わせてしまった感ある。
ここもいくつかお風呂があっておもしろかった。気持ちよかった。

四日目

帰る日。寄り道とか。

起床。あまりにも寒すぎた。あまり寝れなかった。外を見ると。ちなみに、今回の車3台すべて夏タイヤである。帰れないかもしれないと思い出す。目が覚める。

f:id:mattyan1053:20190321143357j:plain
10cmくらい積もってた

f:id:mattyan1053:20190321143428j:plain
真っ白

そんなに標高高くないし3月だし大丈夫やろ~wwってなめてたら降ってびっくり。そら寒いわ。外を散歩がてら様子見。コテージの前は誰も通ってないけれど、道へ出れば轍もあって大丈夫そうと考える。

朝ごはんはパンとスクランブルエッグとコーヒー。安定。とりあえず片付けを始めた。
ぼくは自分の片付けが終わったらコテージ内は他の人に任せて車周りの準備。

f:id:mattyan1053:20190321144433j:plain
窓にお湯をかけて雪を落とす

f:id:mattyan1053:20190321144605j:plain
ピンチ

11時頃ギリギリまで待って出発。雪は止んでて道もマシになっていたのでなんとかなった。富士急ホテルに忘れ物した人は取りに行きつつ忍野八海へ。

忍野八海

富士山が世界遺産になるのと一緒に登録された忍野八海。一日目に行く予定だったけどいけなかったのでお昼前にここへ。
観光公式HP↓

www.vill.oshino.yamanashi.jp

池をまわりつつお土産を購入。ぼくは信玄餅と日本酒飲み比べというものを購入した。ぽしゃけ~。

f:id:mattyan1053:20190321145018j:plain
建物がいい味を出している

f:id:mattyan1053:20190321145059j:plain
水車ってよくない?

お土産を書い終わったらお昼ご飯へ

昼食

ほうとうが有名なのでお店予約して食べに行った。お店はここ↓

wajuurou.com

まじでうまかった。ちゃんとしたところで食べるのもよき。

f:id:mattyan1053:20190321145424j:plain
ほうとう

サイクリングとか

帰るにはまだ早い時間だったのでレンタサイクルのところにいってスワンボートだったり自転車を借りた。 8人スワンボート、4人自転車。
スワンボートのひとは鳥の餌とかも買ったりしてた。

借りたとこ↓。気のいいおじいちゃんがやってた。 www.yamanakako.gr.jp

f:id:mattyan1053:20190321145622j:plain
スワンボートを漕ぐひとたち

ぼくは自転車だった。一人乗り自転車2台と二人乗り自転車1台を借りた。二人乗りはなかなか難しい。前に座った人はコントロール、後ろの人は火力って感じ。重心があってないとふらついてしまう。

f:id:mattyan1053:20190321145746j:plain
二人乗り自転車を漕ぐひとたち

山中湖畔を走ってたんだけどサイクリングロードもあって気持ちが良い。景色もなかなか。時間的に富士山は逆光だったけれど・・・・・・。

f:id:mattyan1053:20190321145910j:plain
後光のさす富士山

途中、二人乗り自転車の後輪がズレて走れなくなるというアクシデントが発生した。山中湖畔3/4くらい走ったあたりかな。スワンボート勢に車で拾いに来てもらって帰還。特に追加料金も取られることもなく優しい人だった。

ここでメンバーの一人がぼそっとえぐいこといったのは心に留めておく。

帰路

ここからは帰るだけ。途中小田原のあたりで夕飯を食べて、静岡の実家に帰るやつをおろしていく。夕飯はなぜか焼き肉。バイキング形式の焼肉店、初めてだった。

ここまでの経路はなかなかえぐいところを選択してしまった。僕は楽しかったけど申し訳なかったと思っている(各々ナビに従って別ルートいくかとおもっていた)。

f:id:mattyan1053:20190321150745p:plain
わるかったとおもっています

すたみな太郎フレスポ小田原店↓

t-stamina.jp

まあ値段相応といった感じ。深くは言うまい。

f:id:mattyan1053:20190321150830j:plain
自分でとってくる

ご飯食べたら解散、車ごとに帰る。おつかれさまでした!

感想

12人で行くってなったときは正直どうなることかとおもっていたけど案外なんとかなった。12人がみんな意見したりすることはないとは思っていたとおりで、4、5人くらいで話し合ってあとはみんなついてこい形式だったのだけど、特に文句もなくついてきてくれたりしたのでつつがなく進んだ。協力感謝。

こうして大人数でいくのもワイワイしていて楽しいね。最終的には4万円くらいしか3泊4日なのにかかってないし費用に対して濃いいいものになった。またそのうちやりたいね。

今回ぼくはずっと運転していたけどなかなか楽しかった。運転できる人増えるといいなあ。みんな練習しよう!レンタカー保険モリモリでいいから!

【GCP】Minecraft鯖の構築とDiscord連携【Minecraft】【Discord】

やりたいこと

Google Cloud Platform(以下GCP)」の「Google Compute Engine(以下GCE)」を用いてMinecraftサーバーを構築したい。常時起動していてはお金がかかって大変なので、遊ぶときだけ起動したいが、自分以外の人が遊ぶときでも起動できるようにはしておきたい。そこで、Discordのチャットにコマンドを打ち込むことで、そのときだけMinecraftサーバーを起動できるようにしたい。

経緯

今まで自分のPC上にサーバーを構築して、ポート開放一時的にしたりとかしてマイクラマルチプレイをしていた。けれども、これではぼくがPCを起動しっぱなしにしなければならないし、他の人が自発的に遊べないのでクラウドとかにサーバーがほしいとおもった。

あとセキュリティ的怖さも・・・・・・ね。

サークルの方ではWeb管理係なるものになったようだし、サーバー関係のお勉強も兼ねて構築してみることにした。

クラウドサービスの中でGCPを選んだ理由としては、GCPが初回一年は約3万円分ほど無料で使えるのと、小さな仮想マシンであれば一年目以降も無料で使えるようだったので、有料分はマイクラ鯖用、無料分はDiscordのコマンドbot用に使えそうだということで試すことにした。(あと既存のアカウントで使えたというのもある)

前提

Googleアカウントを持っていて、GCPが利用できる状態になっている(クレカ登録とかが済んでいる)こと。GCEのインスタンス作成まではできる状態であること。

Minecraftサーバーの構築

こちらは自分のパソコンでサーバーを立てていたのとほぼ同じ手順でやっていたのでさほど問題なく構築できた。
基本的には

cloud.google.com

の通りにやっていく感じ。

インスタンスの作成

イクラサーバーは無料分のスペックの低いインスタンスだとうまく動かすのは難しいようなので、「n1-standard-1」を選択。

OSは個人的趣向でCentOS7。まあお好きなLinux使えばいいんじゃないでしょうか。追加ディスクはやってないのでデフォの10GBのみ。

あとでファイアウォールでポートの設定をするためにネットワーキングタグの設定も忘れずに(あとでもできるけど)。

f:id:mattyan1053:20190218195012p:plain
インスタンスの作成

静的IPアドレスの設定

IPアドレス変わりまくるとめんどうですよねって話。 GCPコンソールの左上からメニューを開いて、「ネットワーキング」→「VPCネットワーク」→「外部IPアドレス」で、「静的アドレスを予約」をクリック。適当な名前を設定したらインスタンスと同じリージョンを選択して、接続先に先程作成したインスタンスを設定しましょう。

サーバー本体の設定

インスタンスを作成したら起動します。必要なものをインストールしていきましょう。 SSHで接続する。GCPのコンソールからでOK。「SSH」をクリッククリック。

# スーパーアカウントにする
$ sudo su -
# yumのアップデート
$ yum update
# 必要なものをそれぞれインストールする
# Javaのインストール。JREあればOK。yum serach javaとかすれば出てくるはずなので自分にあうものをインストール。default-jreとか?とりあえずここではJDK
$ yum install java-1.8.0-openjdk
# バックグラウンドで動かしてほしいのでscreen
$ yum install screen
# 新しくディレクトリを作ってそこにサーバーをダウンロード
$ cd /home
$ mkdir minecraft
$ cd minecraft
$ curl -OL マイクラサーバーのURL
# 初回起動からeula.txtの書き換え
$ java -Xms1G -Xmx3G -d64 -jar server.jar nogui
$ vi eula.txt # 変数eulaの値をfalseからtrueに。

これでOK。サーバーのURLは

Minecraft Server Download | Minecraft

で確認してやりましょう。

ファイアウォールの設定

Minecraftはデフォルトだとポート25565を使うので許可しておかないといけない(変更したければserver.propertiesを書き換え)。 「ネットワーキング」→「VPCネットワーク」→「ファイアウォールルール」を開き、「ファイアウォールルールを作成」する。ターゲットタグのところに、先程インスタンスに設定したネットワーキングタグを設定しておけばよい。ソースIPの範囲は0.0.0.0/0、許可するのはtcp:25565。これで作成すればOK。

f:id:mattyan1053:20190218200612p:plain
ファイアウォールルールの作成

インスタンスの起動、終了時に自動でMinecraftサーバーの起動、終了を行う

インスタンスの設定で、カスタムメタデータのところを編集すればOK。

startup-scriptというキーで値を

cd /home/minecraft
screen -d -m -S mcs java -Xms1G -Xmx3G -d64 -jar server.jar nogui

というものを作成する。これで起動時にサーバーの起動もしてくれる。
次にshutdown-scriptというキーで

screen -r -X stuff '/stop\n'

を追加しておくことで、シャットダウン時にサーバーも正常に終了してくれるようになる。

バックアップ関係

今回ぼくの作ったサーバーでは、こまめにサーバーを起動したり終了したりするように設定しているので、shutdown-scriptのほうでバックアップのスクリプトを追記することでバックアップを行っている。これは各自好きなようにするとよいだろう。crontabとか使うところも多いのではないだろうか。

サーバー起動botの作成

次に、Discordのチャットを見てサーバーを起動するbotを作成した。使うのはPython3とGoogle API。 参考にしたのはここ。

qiita.com

ここのbot.pyを使わせてもらいました。

インスタンスの作成

こちらはDiscordを監視してコマンドが入力されるのを待っているだけなのでGCP無料分を使えばOK。「f1-micro」を使いましょう。リージョンとかも無料分のやつをよく確認して設定。OSも好みのやつを。ここではCentOS7。

APIのアクセスは、デフォルトアカウントでいいので完全アクセス権を許可しておく(このbotのみ動かすならcompute engine 許可されていればいいけれどまあ他の操作するbotも作るなら適宜変更)。ファイアウォールはHTTPトラフィックを使うので許可しておきましょう。

f:id:mattyan1053:20190218202251p:plain
Discord用インスタンスの作成

Botサーバーの設定

先ほどと同じように必要なものをインストールしていく。

$ sudo su -
$ yum update
# Python3の確認。
$ pyhton3 -V
$ yum search python3.7
$ yum install python3.7 # 出てきたものをみて適宜変える。
# discord.pyを導入する
$ python3.5 -m pip install  -U discord.py
# google APIを使うためのパッケージも導入
$ python3.5 -m pip install --upgrade google-api-python-client

Google Cloud SDKのインストール

ほとんど参考にした記事通りにやればいいのでほとんど割愛。ここではCentOS7を使っているのでwgetの代わりにcurl -OLを使うことに注意。

また、gcloud initでは、GCEインスタンスを使っているのでデフォルトアカウントを選択(たぶん対話型設定で[1]を選択すればよいはず。)でOK。外部のコンピュータからgcloudを使う場合は面倒な手順(認証作業)があるようだけど、同プロジェクト内だし楽みたい。「warning」と出るかもしれないけど特に問題はない。auth認証も特にいらない。

Discord Botの作成

DiscordのBOTを作るには登録してこないといけない模様(当然)。おとなしくやります。

qiita.com

これに従ってBOTを作成してトークンを取得。取得できたら、

$ vi ~/.bash_profile
$ export BOT_TOKEN='取得したトークン'

として登録しておくと、上記サイトの「bot.py」が使えるようになります。

botの起動

実際にbot.pyを動かしてみます。まずはサイトのやつをコピペさせてもらって(@thincellerさんに圧倒的感謝)、任意のフォルダに配置。そしたら、

nohup python3.5 bot.py&

で起動したままにできます。

2020/12/22 追記

2年たってこれを見直してみるといくつか問題があったので追記

問題1: pythonが動かない

discord.pyがアップデートされていて上記コードだと動かない!!新しくなったdiscord.py用にコードを修正しました。コチラ↓

#!/usr/bin/env python3
import asyncio
from time import sleep

from googleapiclient import discovery
import discord

client = discord.Client()

BOT_TOKEN = YOUR_TOKEN

# Instance information
PROJECT = 'YOUR_PROJECT'
ZONE = 'YOUR_INSTANCE_ZONE'
INSTANCE = 'YOUR_IUNSTANCE_NAME'

# Build and nitialize google api
compute = discovery.build('compute', 'v1')

@client.event
async def on_ready():
    print('Logged in as')
    print(client.user.name)
    print(client.user.id)
    print('------')

@client.event
async def on_message(message):
    if message.content.startswith('/minecraft'):
        # ex.) message.content: '/minecraft start' => command: 'start'
        command = message.content.split(' ')[1]

        channel = message.channel

        if command == 'start':
            m = await channel.send('Server starting up...')
            start_server(PROJECT, ZONE, INSTANCE)
            await m.edit(content='Success! Server started up.')
            sleep(10)
            await m.delete()
        elif command == 'stop':
            m = await channel.send('Server stopping...')
            stop_server(PROJECT, ZONE, INSTANCE)
            await m.edit(content='Success! Server stopped.')
            sleep(10)
            await m.delete()
        elif command == 'status':
            status = get_server_status(PROJECT, ZONE, INSTANCE)

            if status == 'RUNNING':
                m = await channel.send('Server is running! Please enjoy Minecraft!')
                sleep(10)
                await m.delete()
            elif status in {'STOPPING', 'STOPPED'}:
                m = await channel.send('Server is stopped. If you play Minecraft, please chat in this channel, `/minecraft start`.')
                sleep(10)
                await m.delete()
            else:
                m = await channel.send('Server is not running. Please wait for a while, and chat in this channel, `/minecraft start`.')
                sleep(10)
                await m.delete()
        elif command == 'help':
            m = '''
            ```Usage: /minecraft [start][stop][status]
    start   Start up minecraft server
    stop    Stop minecraft server
    status  Show minecraft server status(running or stopped)```
            '''.strip()
            await channel.send(m)

def start_server(project, zone, instance):
    compute.instances().start(project=project, zone=zone, instance=instance).execute()

def stop_server(project, zone, instance):
    compute.instances().stop(project=project, zone=zone, instance=instance).execute()

def get_server_status(project, zone, instance):
    res = compute.instances().get(project=project, zone=zone, instance=instance).execute()
    return res['status']

client.run(BOT_TOKEN)

コレで動きました。

問題2: Pythonでエラーが出た時止まってしまう。

nohupだとエラーがでると実行が止まって動かなくなってしまう。ってことでサービス化した。 /etc/systemd/system/mc-discord-bot.service

[Unit]
Description = Discord bot deamon

[Service]
User = username
ExecStart = /path/to/bot.py
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target

bot.pyの権限、chmodしておきましょうね

あとは起動

#!/bin/bash

$ sudo systemctl reload
$ sudo systemctl enable mc-discord-bot.service
$ sudo systemctl start mc-discord-bot.service

できるようになったこと

GCP上に立てた有料インスタンスMinecraftサーバーは、使ってないときはシャットダウンした状態。誰かが遊びたいときにDiscord上のテキストチャンネルで「/minecraft start」とすれば起動できるようになった。逆に、「/minecraft stop」とすればシャットダウンしてくれる。これで有料インスタンスを必要なときだけ起動できるようになったので、お金が節約できるようになった。よかった!