注意事項 ( 言い訳 )
適当で汚い実装が多いので、参考にすべきかは微妙な感じです。( なら何故公開する!?)
このスライドの内容は前作 FarmFury! やブログで解説した事はあまり載せていませんので、良ければそちらも参照ください。 Farm Fury! 技術資料 最後に参考リンクが貼ってあります
チーム情報
Max Neet Games ( 訳 : Hatarakitakunai-De-Gozaruuuu!!)
活動概要 まったりとゲーム作ってます。きっと、多分、恐らく 作ったゲームの資料も公開していく予定
ホームページ http://maxneet.web.fc2.com/
メンバー 大熊猫 : プログラム、惰眠担当 まる : アート、雑用担当
幽霊の棲む家紹介
プラットフォーム PlayStation Mobile にて 108 円で配信中
プレイ人数 : 1
ジャンル : ホラー Point & Click Adventure 的ホラーゲームです
想定プレイ時間 : 20 ~ 60 分
開発環境 ( ほぼ前作のコピペ )
PC XNA 4
PlayStation Mobile PsmStudio MonoGame
全体 LuaInterface (C#でLuaを使う為のライブラリ) Tortoise svn, Audacity, Paint.Net, Excel, etc
ファイル管理 Dropbox上にsvnのリポジトリ作成し、Dropboxのフォルダを共有
まずは XNA でPC 用に作り、そこから Psm対応しました。
MonoGame についてはFarmFury! の技術資料でザックリと説明しています。
テーブル
Excel で作成したテーブルはその後に自作のコンバーターを使って xml に変換しています。
今回 xml は XNA の Intermediate Serializer を使用せず通常のC# の機能で扱っています。
実装の紹介 - C#メモ – xml読み込みの汎用化
正直 C# の言語機能を使うと遅いのでバイナリーに変換すべきでしたが、面倒なのでやってません ( おぃ)
今回のゲームはデータ量事態そんなに多くないので、日記以外( と Debug ビルド時のデバッグ用テーブル ) のテーブルは起動時に読み込んでいます。
Excel Converter
C# + OpenXMLで作成しています。
プログラムの引数に変換するファイルを探す場所と吐き出し場所を指定する様にしています。
探す場所に指定したディレクトリにある全てのxmls ファイルと、子フォルダにある全てのxmls ファイルを変換する。
クラスの名前はファイル名をそのまま使っています。
バッチ
毎回 Excel ファイルを D & D するのは面倒なので、バッチで纏めてやる様にしています。
バッチの内容は、指定ディレクトリにある全てのファイルを変換し出力するのをWindows 版、 Psm 版、 Windows 版ビルド先のデータフォルダに行っています。
例 : Tables.bat
SET EXCEL_CONVERTER=%HORROR_ROOT%\Executable\ExcelConverter\ExcelConverter.EXE SET PSM_CONTENT_DIR=%HORROR_ROOT%/Program/Psm/Horror/Content/Tables SET WIN_CONTENT_DIR=%HORROR_ROOT%/Program/Windows/Horror/HorrorContent/Tables SET WIN_EXE_DIR=%HORROR_ROOT%/Program/Windows/Horror/Horror/bin/x86/Debug/
Content/Tables
"%EXCEL_CONVERTER%" "%CD%" "%PSM_CONTENT_DIR%" "%EXCEL_CONVERTER%" "%CD%" "%WIN_CONTENT_DIR%" "%EXCEL_CONVERTER%" "%CD%" "%WIN_EXE_DIR%"
バッチ ( 続き
例のバッチはテーブルが置いてあるフォルダに置いてあり、今のフォルダのディレクトリをファイルを探す場所として Converter に渡しています。
Converter があるパスの定義と、3つの吐き出し先を定義し、3回コンバート処理を行っています( 効率?ナンデスカソレ? )
事前準備として、 %HORROR_ROOT% の環境変数をプロジェクトのルートフォルダに定義しています。
フラグ / 変数
周りクドイ実装ですが、変数保存用のクラスを作り、 enum でその変数の種類を定義しています。
変数の保持自体は Dictionary に変数の種類を保存し、文字列を keyに使っています。
文字列は定義用の enum を ToString() で変換すると言うアホをやっています (int とかでよかったんじゃね? ) 。
保存用のクラスにはセーブ用に ToString() をオーバーライドして中身を書き出せる様にしてあります。
余談ですが、 enum の定義は必要以上に多く取って置き、後で増やす必要が出てもセーブデータのサイズが変わらない様にしています。 なので実はセーブデータ ( と書き込む内容 ) を 80% ぐらい削れる気がする
変数保持用のクラスpublic class VariableTable<T>{
protected Dictionary<string, T> m_variables;
//… 変数足したり、設定したり
public override string ToString() { string data = ""; foreach (KeyValuePair<string, T> kvp in m_variables) { data += kvp.Value.ToString() + "\n"; }
return data; }}
フラグ定義の enum
public enum Flag{
// -- ドア系BasementDoorLock, // 地下室のドア使える?BathDoorLock, // 風呂のドア鍵が掛かってる?KitchenDoorLock_0, // 廊下から台所のドア鍵が掛かってる?KitchenDoorLock_1, // 書斎から台所のドア鍵が掛かってる?LibraryDoorLock, // 書斎へのドア鍵が掛かってる?FrontDoorLock, // 外へのドア鍵が掛かってる?
// … その他色々
// -- イベント系HasWatchedNormalEnding, // 通常エンディングを見た?
IsTrueEnd, // 良いエンディング
Max = 100 // 本当は 30 以下だけど多めに取る}
インスタンス生成 [Serializable]public class GameData
: Singleton< GameData >{
public const bool _DEFAULT_FLAG = false;public VariableTable<bool> Flags { get; private set; }// … その他の宣言
public GameData(){
Flags = new VariableTable<bool>();
// 空データ追加for ( int i = 0; i < (int) Flag.Max; ++i ){
Flags.Add( ((Flag)i).ToString(), _DEFAULT_FLAG );}
// … その他の初期化}
public override string ToString() { string data = Flags .ToString(); // … その他のデータ return data; }}
ゲームオブジェクト
ゲームオブジェクト = ゲーム内に存在している物
ただ、このゲームの場合は基本的にはゲーム内で描画される物 = ゲームオブジェクト、みたいな扱いになっています
Unity やら Cocos やらその他色々で使われている親子関係っぽい物を使用し、親から情報を受け取ってます。
ただ、実装時に怠けた為位置関係のみ親から受け継ぐと言うアホ仕様
(はい、反省します・・・ )
コンポーネント等を使用しない為、基礎クラス内に位置やシンプルな描画、クリック / タッチ確認処理を実装
ゲームオブジェクト実装
木構造の基本的な実装
List<GameObject> Child 見たいに直接の子供一覧を保持
更新描画は再起的に呼び出して木構造全体で呼ばれる様にしています
ローカル座標 ( 自身の位置 ) とワールド座標 (親の影響を受けた後の位置 )両方を取れる様にしています
ゲームオブジェクト実装2
更新処理:protected List<GameObject> m_child;public virtual void Update(float delta) {
for ( int i = 0; i < m_child.Count; ++i ) {
m_child[i].Update( delta ); // … その他の処理
} }
public virtual void Update(float delta) { for ( int i = 0; i < m_child.Count; ++i ) { m_child[i].Update( delta ); if ( m_child[i].IsDead ) { m_child[i].ClearAll(); m_child.RemoveAt( i ); i--; } } }
部屋の構成
部屋の背景は1枚絵で表示しています
部屋自体も GameObject で、子供としてキャラ等を配置
それ以外にも各種個別に使用したり、何らかの判定をしたりする物は子供を追加する時に別のリストにも追加しています。
部屋のインスタンスはゲーム全体の状態遷移管理をしている中にある、ゲーム中状態 ( と呼べば良い? ) の中で生成され、管理されています。
部屋の移動用データは部屋作成時に読み込んで設定しています(移動処理の説明時に紹介します ) 。
部屋の電気が切れてる状態等の演出用に他のオブジェクトの前に描画出来るフィルター的な物を設定出来る様にしています。
部屋の配置物
背景オブジェクト (画像有り ) 表示の為だけに配置し、状況によっては追加したり、しなかったりする
選択出来るオブジェクト (画像有り ) カーソルがルーペに変わる置物。ドア、棚、アイテム等はこれです。
トリガー (画像無し ) 表示はされませんが、キャラが触ったら処理を呼び出すオブジェクトです。腕発見時等に使用しています。
追尾者用トリガー (特殊 ) プレイヤーに付いて来るキャラが触ったら処理が呼び出されるオブジェクトです。主に成仏イベント用です。
フィルターの前に描画するオブジェクト フィルターで電気がついていない演出をしている時に電気のスイッチを前に持っ
てくる時等に使用しています。
部屋の設定
部屋の構造、背景、等をどうするかは部屋を作成した際にその部屋用のスクリプトを走らせて初期化しています。
フラグの状態によって結構中身が変わるので、出来るだけプログラム内に進行に関わる処理を含めない為にスクリプトに投げています。
本当はエディター作ろうかと考えてましたが、基本的に自分 ( プログラマー ) がイベント実装していたので、面倒になり妄想で終わりました(またまた反省しています・・・ )
部屋の切り替え
このゲームは移動先を選択、もしくはオブジェクトを選択した際にそこを目的地として移動します。
目的地に着いた際、選択先がオブジェクトの場合そのオブジェクト内の処理が呼ばれます。
ドアについた場合、まず退出演出 ( ドアにズームイン等 ) 、その後に部屋を保持しているオブジェクトに部屋の切り替えしろ指令を出しています。
ちなみに、ドア以外についた場合は、大体の場合はオブジェクト生成時に指定しているスクリプトを呼び出しています。
部屋を移動した際のキャラの位置は元々来た部屋の ID を保持し、その部屋へ繋がっているドアを探しそこの周辺に設定しています。
部屋の切り替え(内部事情暴露 )
部屋移動時の演出にズームとフェードがあります
理由としてはズーム時にはドアを黒い四角に表示を変えているのですが、角度があるドアや、階段等では上手く合わせるのが面倒 & 違和感が出る為です ズームして拡大した時の表示もかなり違和感が出てるの
で、そこも大きいです
なので違和感を覚える様な所はフェードで誤魔化しています・・・ごめんなさい
スクリプト
このゲームでは Lua を使用してイベントの実装や部屋の初期化を行っています。
コアルーチンを Psm で楽に動かす方法が解らなかったので、泣く泣く毎フレーム Update() 的な物を回していました・・・ orz 超面倒なのでやめましょう・・・!
スクリプトの使い方としては、 C# 内にイベント等で使用出来るメソッドを用意し、 Lua 内から呼べる様にしているだけです
スクリプト使用例
部屋にオブジェクトを追加、削除 進行によっては鍵等のアイテムを配置 クリック対応範囲の指定もスクリプトから生成時に行っています。
キャラの移動、速度変更、等の設定変更
フラグ設定、変数設定
SE再生、 BGM切り替え、一枚絵演出、等演出系全般
別のスクリプトを次に呼ぶ予約 部屋に入ったらイベントを開始したい場合、部屋の初期化の最後
に次のスクリプトを指定して予約する
スクリプトの種類
ゲーム内には大まかに分けると、3種類のスクリプトが有ります
1. 部屋の初期化 既に説明した様に部屋に入った際に初期化を行う
2. オブジェクト選択時用のスクリプト 部屋に配置するオブジェクトにプレイヤーが辿り着いた時に呼び出す
スクリプト。中で通常のセリフ等を呼ぶか、イベントを開始するかの判断をしている時もあります。
3. イベントのスクリプト 主にプレイヤーが操作出来なくなる演出処理の進行を行っています。基本的にはイベント番号 = 進行度で管理しています (単純なゲームで良かった・・・ホッ )
部屋初期化のスクリプト例function Update()
-- 進歩獲得progressID = GetProgressID()
-- データ読み込みLoadTexture( "Textures/Rooms/room0/background", "bg0" ) LoadTexture( "Textures/Rooms/room0/Hammer", "Hammer" )
-- 背景設定SetBackground( "bg0" )
-- カメラ移動出来るかの設定SetScrollable( false )
-- === 物配置 ===
if IsPlaceHammer() thenAddBackgroundObject( "Hammer", 701, 381,
0, 0, "" )End
-- 工具箱AddObject( 151, 316, 178, 158, "Objects/Room0/Toolbox0" )
if progressID == 0 then -- ゲーム開始時else
-- 出口追加AddStairs( GetCorridowID(), false, 475, 155,
137, 252, "" )
-- 棚AddObject( 665, 0, 142, 476,
"Objects/Room0/Shelve0" )
-- ポスター右AddObject( 213, 105, 97, 102,
"Objects/Room0/Poster0" )
-- ポスター真ん中AddObject( 101, 105, 97, 150,
"Objects/Room0/Poster1" )
-- ポスター真ん左AddObject( 0, 61, 73, 164,
"Objects/Room0/Poster2" )end
-- スクリプト終了if progressID == 0 then
PlayNextScript( "Events/Event0000" )else
OnEndScript()end
イベント
ゲームの進行度を見る用に ID を保持 先ほど紹介した変数の所で保持
主に現在の進行度とフラグ設定をスクリプト内で見て、各設定を行っています
イベント中はカーソルやタッチの入力に対応しない様にし、セリフの更新はスクリプトから確認、対応しています
移動処理 ( 経路探索 )
移動処理には A* (えーすたー ) と言う経路探索を使用しています。
移動できる場所を保持し、現在地から目的地への経路を探索します。
現在地はキャラの位置、目的地は選択した位置 or オブジェクト
移動範囲は部屋をマス目で区切り、移動出来るマスにはソレ用の値を設定し、出来ない箇所には値が無い用にしています。
移動範囲 ( マップ ) 設定
別の適当なプログラムをでっち上げました
部屋を 32 x 32 のマスで区切る
ピンクになっている所が移動可能なマス 左クリックで移動可能に 右クリックで不可能に
設定を書き出したり、読み込んだり出来ます。 吐き出したファイルはそのまま
ゲームに使用されています。
A* ( えーすたー )
ゲーム内のマップの各マスには、位置、歩けるか、隣接してるマス、目的地までの距離、道筋に追加したマスへの参照、既に確認/計算が行われたか?、等が保持されています 目的地用は随時計算
やる事は開始位置のマスから隣接している各マスを延々と回し続け、「一番目的地に近いマス」を探し続けて道のりを計算します
移動可能経路の総当りでは無く、大まかな距離計算を行う事で、検索する範囲を狭めた上で早い経路を見つけるアルゴリズムです
目的地計算の為、「通れるマス」のリストと、「一度確認したマス」のリストを計算時に保持し、使用しています。
A* ( えーすたー ) 処理1
1.探索に使う値とリストを全部リセット2.開始位置と目的地までの大よその距離を計算します。その後に通れるマス一覧に追加します Heuristic Function (H) と言う方法で計算します 基本以下の様に計算します :
private float Heuristic(Point point1, Point point2){ return Math.Abs(point1.X - point2.X) + Math.Abs(point1.Y - point2.Y);}
A* ( えーすたー ) 処理2
3.現状通れる事になっているマス一覧を見る物が無くなるまで処理を回す (while とかで ) 初回は開始地です
4.一覧の中から目的地に一番近いマスを探す 一覧が空だったり、何も見つからなかったら検索を終了し、検索結果として空値を返す
debug 時は assert とかでもいい気がします 1度目は開始地点なので、探すも何も無いです
5.獲得したマスが目的地の場合、道のりを再度定義して結果として返します マスに保存してある自身を追加したマスの参照を辿り、来た道のりを逆算しま
す。その位置情報を順番に List<Vector2> として返し、これを移動の道のりとしています
違った場合は6へ続きます
A* ( えーすたー ) 処理3
6.もし獲得したマスが通るマス一覧、もしくは確認済みマス一覧に無かった場合、獲得したマスの全ての隣接したマスで以下を行う(移動可能のマスのみ ) 開始位置からの距離をマスに書き込む(開始から現在のマスまでの移動距離 + 1 ) ゴールまでに掛かる距離を書き込む
( 上記の値 + Heuristic ) その隣接マスを通れるマス一覧のリストに追加 その隣接マスをリストに追加したマスとして、現在のマスへ
の参照を設定
[*]既に追加済みの場合は7へ飛ぶ
A* ( えーすたー ) 処理4
7.もし今見ている隣接しているマスの目的地までの距離が現在の移動距離より長かった場合、6と同じ事をする 正し、移動予定と確認済みの一覧リストには追加しない。
8. [4 ] で獲得した今見ているマスを確認済みリストに追加する
A* ( えーすたー ) 処理5
9.ループ一回りが終わり、この時点では前回のマスの隣接マスで移動出来る物が移動予定リストに入っているので、そこからまた4~8を繰り返します。
後は4の失敗か、5の成功まで繰り返します。
長い上に解りづらい・・・
目的のマス指定
移動する際、選択した位置のマスを計算し、経路探索を行っています。
オブジェクトは移動出来ない場所にある場合が多いので、各オブジェクトの選択時に移動先を獲得する処理が入っています デフォルトでは、オブジェクトの真ん中の一番下の位置を返
しています。 そこから移動先マスを計算する際、同じ位置にあるマスか、
そこから下方向にマップを検索していき一番最初に見つかった移動出来るマスを返す様にしています。
もし移動先が無かった場合、左右に一列づつずれて同じ様に下方向に検索する事を繰り返しています。
実際の移動
経路検索で獲得した List<Vector2> のリストを頭らから順番に目的地として移動して行きます。
ここは特に特別な事はしておらず、目的のマスへの向きを計算し、向き * 移動速度で移動しています。
キャラが画面奥に移動すると小さく、手前に来ると大きくなる様にしています これは単純に Y座標と適当な数値を架けてスケールを計算しています
NPC もプレイヤーと同じ目的地に向かって移動しているだけです 速度を遅くして誤魔化しています・・・ でも移動をやめるとプレイヤーと重なります orz
日記画面
ゲームの状態 ( ステート、シーン ) はブログで紹介しているゲームの状態変更の実装方法 を元に作成しています
日記を開いた際には新たに日記用の状態を追加し、そこに切り替えています
日記画面の描画時に元々の画面も表示する様にして元の画面の上に表示する様にしています
日記終了時に元々の状態に戻す様にしています
日記のページめくり実装
まず空のページをめくる側の位置に新たに空のページ画像を作ります。
それを徐々に小さくしていきます 左側の場合はサイズと位置を調整 右の場合はサイズのみ
同時に、めくっているページの画像もサイズと位置を変更していく と同時にフェードもしていく
ページが真ん中 (横幅0 ) になったら、今度はページの画像表示を反転した上で横幅を増やしていきます。
ナイスごまかし!
セーブ / ロード
Psm のサンプルと某ゲームを参考にしています。
フラグ等を管理しているクラスの情報を ToString() で書き出し、 byte[] に変換しています。
Byte に変換したデータを xml に保存しています。
書き込み時に画面が動かないと止ってる感があるので別スレッドで作業をし、待ってる間は描画を微妙に変える様にしています [Saving…] の点の数を1~3の間で変えて行ってます 実装はブログで紹介しているマルチスレッドでコンテントの読み込み と
XNAの機能を使ってセーブする方法 の似たような処理を行っています ( セーブ / ロードには XNA 的な機能は使ってません )
非同期処理をデータ読み込みと同じ様にセーブ管理の基礎クラスで行い、継承したクラスの Process() 内でセーブをやっています。
デバッグメニュー
ゲーム内から1ボタン / キーでデバッグメニューに飛べる様にしていました
デバッグ画面中はゲームの進行は止っています (更新、描画が呼ばれていません )
面倒だったので、見かけと操作は適当です・・・
デバッグ機能一覧 進行度表示/設定
言語設定 結局日本語しかないので、基本未使用
イベント選択 指定したイベントを再生 部屋のやキャラの位置に不一致が起こるので、専用のテーブルを用意し、それを参照する様にして設定を行ってい
ます なので、ある意味手書きで設定しています・・・
フラグ設定 フラグのTrue/Falseを切り替えられます
変数(数値)設定 ゲーム内の変数の数値を変更できます
アイテム設定 アイテムの追加、削除が出来ます。ゲームでは各アイテム1つのみしか手に入りませんが、一応最大数まで増やす
事も出来ます( 0~99) UIにも反映する処理を行っています。
テーブル再読み込み ( 次のスライドへ続く・・・)
テーブル再読み込み
テーブルを再度読み込む事で、ゲーム実行中でもデータ修正を反映出来る様にしています。
やってる事自体は簡単で既に読み込んでるデータを破棄し、再度読み込むだけです。 Excel で修正 > バッチ使う > 再読み込み > 反映!
最初の方に説明したバッチで出力先にビルド先のフォルダも含めているのは、この流れを簡単に行う為です。
ちなみに、 Lua のスクリプトもスクリプト実行時に読み込んでいる為、編集して再度イベントを開始すればゲーム起動したまま修正確認が出来ます。
部屋の情報表示機能
前のスライドの画像にある様に、部屋の情報表示を切り替えられる様にしています。
歩ける範囲 /マス ピンク色の部分
オブジェクトの当たり範囲 / クリック範囲 緑と、青の部分。 クリック可能の場合はルーペに変わる範囲 画像にはありませんでしたが、トリガーも表示 表示は 1x1 の画像を当たり範囲に描画しています
参考 Farm Fury! 技術資料
XML読み込み Xml読み込みの汎用化
メニューの作り方 メニューの作り方 メニューの作り方3:サイズが変わるメニュー
ゲームステート(シーン遷移)の作り方 ゲームの状態変更の実装方法2 (クラス編)
ゲームの状態変更の実装方法3 (クラスの再利用化) 会話のテキスト表示系
流れるテキストの作り方 自動改行するテキストボックスの作り方
エフェクト 振動エフェクトの作り方 パーティクルについてはFarmFury! の時と同じです
カメラ 2Dゲーム用のカメラの作り方
アイテムを拾った時の演出等で使うTween系(Lerp系)処理 Lerp系処理を流用する方法 Tween(Lerp系)処理後に何かをする方法
当たり判定 2Dゲーム用の当たり判定
デバッグ FPSカウンターの作り方
オマケ : これは作っておくべきだった集
自動通しプレイ機能 デバッグ大事! 1度進行不可で審査落ちた!
エディター スクリプト全部書くとミスしやすい 他の人に丸投げ出来ない Lua だとインテリセンス (ゆとり ) が無いから・・・
リアルタイムでのライティング 画面の暗さ調整がアホみたいだった・・・
オマケ2:作ったけど使わなかった
翻訳対応 翻訳する時にそなえて英語用のテーブルや画像
を変える処理もデータのフォルダや仮データも準備はしてありますが、結局使っていません。
自分でやるのはメンドイです(英語書くのは苦手・・・) 実装は言語によって読み込み先を変える様にし
ています
最後に
紹介されていない物で知りたい事や、質問、疑問、ツッコミ、等があればご連絡ください
Twitter: @ookumaneko_XD
ブログ : http://ookumaneko.wordpress.com/