今回は長いです。
[nico]
http://www.nicovideo.jp/watch/sm15660411[/nico]
今までやりたくなかったですが、とうとうリプレイに取り掛かる時がきました。
リプレイって大変そうじゃないですか。あくまでイメージなんですが。
プレイ時と同じ状況を再現しなきゃいけないとか、リプレイの状態が引き継がれないように注意しなきゃいけないとか。
そういうのがめんどそうと思いFifthCannonではやらなかったのですが、弾幕STGにはやはり必須ですね。
そしてもう一つ、まだ作ってすらいないマップ部分もリプレイに対応したいと思っています。
非戦闘時はメトロイドっぽく隠し通路や隠しアイテム満載のゲームにしたいので、どうしてもリプレイがあったほうがいいかなーと思っちゃうのです。
ただしこれは挫折するかもしれまん・・・。
第一にリプレイをどこで切るかという問題です。
セーブした後、そのまま続けた場合とロードした場合で状況が変わります。
敵が近くにいるとかそういうのです。
なのでそこも再現する必要があるわけです。ここはおいおい考えます。
また、いきなりソフトを強制終了させられたときの対処も考えないといけませんね。
そしてリプレイと共に避けてきたビット演算についても勉強してます。
このゲーム、パッド入力(意味のあるキー入力)の数が13あるので2バイト必要なんですよね。
ちょっと多すぎじゃね?とお思いのアナタ!
ちゃんとキーボードでもできるように考えています。(というか自分がキーボード派)
まず絶対必要な上下左右。
STGではおなじみショット、ボム(このゲームでは魔法のこと)、低速、ポーズ。
そしてたまにしか押さないキー(アイテム、ボム(このゲームにおける)、チェンジ)という構成になっています。
あと二つは、決定とキャンセルです。ショットは押しっぱでいいので、プレステコントローラーでいう○ボタンに割り振られていると指が疲れるんですよね。
だからといってL1やR1にすると、決定する際に結構もどかしくなります。慣れればいいのですが、やはり決定は○がいいなあと思うのです。
ただし○にボムも振られていた場合、決定を押して戦闘突入した直後にボムが誤爆する可能性も・・・。
そこは勘弁してください。
で、肝心のリプレイの仕組みですが、ISLeさんに教えていただいたビット演算によるキー入力の保存をそのまま使わせていただき、MoNoQLoREATORさんのファイルにちまちまとキー入力を保存するやり方でメモリをなるべく消費せずに記録しています。
お二方情報提供ありがとうございます m(_ _)m
ただし記録する場所は一時的な保存ファイルで、最後リプレイ記録が終了するときにリプレイに関する情報などと一緒に、まとめてリプレイファイルとして別のファイルに保存しています。
また、キー入力を見る関数を2つ用意しました。1つはリプレイ再生時にはリプレイの入力情報(別変数で保存)を、通常時は現在の入力情報を渡す関数で、もうひとつはリプレイ再生時も現在の入力情報を渡す関数です。
これにより、再生中もキー入力を受け付けて、龍神録のような倍速リプレイや途中キャンセルを実装しています。
そしてリプレイの実装中、こんなバグに遭遇しました。
リプレイ選択時にカーソルが下に行ったと思ったら上に戻ってしまうという謎のバグです。
一番下に行ったときに上に戻るのは正しいのですが、そうでないのに戻ってしまうのです。
しかも変数に代入するところにブレークポイントで見張ってたのですが、どこもヒットせず。
なんじゃこりゃあ!
変数の値が変更された瞬間にその場でブレークする機能さえあれば・・・。
とtwitterで呟いたら御津凪さんと沖さんに同時にあるよと教えていただきました。
(; ・`д・´) ナ、ナンダッテー!! (`・д´・ ;)
今まで散々探してなかったのに!ググったけど見つからなかったのに!
・・・もしかして日本語悪くてブレークポイントの設定の、変更されたときにブレークポイントでブレークすることを言ってると伝わってるんじゃ!
なんて思ったらそんなことはなかった。
二行前のことは忘れてー (/ω\)ヤメテー
なんでもデバッグ中に、デバッグ→ブレークポイントの作成→新しいデータ ブレークポイント
を選択するとできるそうです!
すげー!みんなも試してみよう!
え・・・?
常識・・・だと・・・?
(´;ω;`)ブワッ
お二人とも・・・真にお手数をおかけして申し訳ありません・・・・。 orz
そしてガンガン使っていきたいと思います! 本当に助かりました。
はいでは早速。
そして謎の行でブレーク。
なんだこりゃ。この行にはその変数は含まれておらんぞ。
・・・
・・・?
・・・・!!!
配列オーバーでした(号泣)
うーんそりゃいくら変数見張っててもわからんわけだ。
あと、ネックだった描画についての実験もちょっとしました。
結果は、描画のサイズがデカければデカイほど時間がかかるという当たり前のものでしたが。
だからどうしてもたくさん弾を出したいときは、小さい弾にするというのも手ですね。
そんなわけでリプレイのコードをのっけたのでよろしければ見てやってください。
宣言がなくて意味不明な変数や関数があると思うのでわからないときはどんどん聞いてやってください。
► スポイラーを表示
CODE:
//replay.h
#ifndef REPLAY_H
#define REPLAY_H
#include "GV.h"
#include "DXLib.h"
#define MAXREPLAY 100 //リプレイ最大保存数
class Creplaybase
{
private:
char name[9]; //名前
unsigned long long int score; //最終スコア
int replaytype; //0通常(マップ含む) 1プラクティス
int randomseed; //シード値
int replayframe; //リプレイのフレーム数
double avaragefps; //平均FPS
public:
Cversion version; //バージョン(自動取得)
DATEDATA nitiji; //日時
void init();
Creplaybase()
{
init();
}
int getframe(){return replayframe;}
void setseed(int seed){randomseed=seed;}
int getseed(){return randomseed;}
char* getname(){return name;}
int gettype(){return replaytype;}
double getavaragefps(){return avaragefps;}
void settype(int num){replaytype=num;}
void plusframe(){replayframe++;}
void setnitiji(){ GetDateTime( &nitiji ) ;}
void setscore()
{ if(replaytype==0)
this->score = allscore;
else
this->score = ::score;
}
void setavaragefps();
};
class CreplayPractice
{
//キャラクター情報(フラグなどセーブデータほぼ全て)も入れる
int practicenumber; //プラクティスのみ、スペル番号
public:
Creplaybase replaybase; //基礎リプレイ情報
void init_tr(int spellnum=0);
CreplayPractice()
{
init_tr();
}
void setpracticenumber(int num){practicenumber=num;}
int getpracticenumber(){return practicenumber;}
};
void play_init_replay(CreplayPractice* prply); //リプレイ再生前の初期化
void rec_init_replay(int type,int spellnum); //リプレイ記録前の初期化
void play_replaypad_mf(); //再生時、毎フレームリプレイのキー入力を再現する
void rec_replaypad_mf(); //記録時、リプレイのキー入力を記録する
void rec_replayend_tr(); //リプレイ終了時の処理をする
void setreplayseed(int seed);
int getreplayseed();
#endif
► スポイラーを表示
CODE:
//replay.cpp
#include "replay.h"
#include "STG.h"
#include "chore.h"
#include
#include
#include
using namespace std;
CreplayPractice replayP; //リプレイ保存、再生用変数
int replayFlag=-1; //0記録中 1再生中 -1その他
int replaycount=0; //リプレイ再生(保存)時間
int replayPad[16]={0}; //リプレイ再生用パッド入力状態
short int old_inputbit=0; //過去の入力状態(ビット演算)
vector Vfps_ave; //fpsの平均を記録していく
int diff_inputtime=0; //キー入力が変化した時間(プレイが始まってからの) リプレイ再生時に使う
short int diff_inputbit=0; //変化したパッドをビット演算で読み込む
int readcount=0; //リプレイを読み込んだ回数
int replay_baisoku=1; //リプレイが何倍速か指定
void Creplaybase::init()
{
GetDateTime( &nitiji ) ;
score=0;
replaytype=0;
randomseed=0;
replayframe=60;
avaragefps=999;
strcpy_s(name,9,"NaNaShi.");
}
//vectorから平均FPSを計算
void Creplaybase::setavaragefps()
{
if(Vfps_ave.size()==0) //一つもなかったら離脱
{
avaragefps=-1;
return;
}
avaragefps=0;
for(int i=0; i 1)
replay_baisoku/=2;
}
}
}
void rec_replaypad_mf() //記録時、リプレイのキー入力を記録する
{
if(replayFlag==0)
{
short int inputbit=0; //入力状態(ビット演算)
for(int i=0; i 0)
inputbit |= 1<< i;
}
if(inputbit ^ old_inputbit) //違った場合inputbitと時間を記録
{
ofstream ofs(".\\replay\\tmpReplay.dat", ios::binary | ios::app);
if(ofs)
{
//書き込み
ofs.write( (char *)&replaycount, sizeof(int) );
ofs.write( (char *)&old_inputbit , sizeof(short int) );
}
else //いずれ消す
printfDx("リプレイ記録失敗");
old_inputbit=inputbit;
}
if(replaycount%FLAME==0)//60フレームに1回FPSを取得
{
if(fpsave!=0)
Vfps_ave.push_back(1000/fpsave);
else
Vfps_ave.push_back(-1);
}
replayP.replaybase.plusframe();
replaycount++;
}
}
void rec_replayend_tr() //リプレイ終了時の処理をする
{
replayP.replaybase.setnitiji();
replayP.replaybase.setscore();
replayP.replaybase.setavaragefps();
Vfps_ave.clear();
string filebase = ".\\replay\\practice_replay\\";
string file;
char num1[3] = {"\0"};
fstream fs;
ifstream ifs(".\\replay\\tmpReplay.dat", ios::binary); //一時保存リプレイを開く
for(int i=0; i < MAXREPLAY ; i++)
{
sprintf_s(num1, "%d", i);
file = filebase + "replay" + num1 + ".dat";
fs.open(file.c_str(), ios::binary | ios::in ); //ファイルがなくても作らせない
if(fs.fail()) //ファイルがない場合ここに記録
{
fs.close(); //一度閉じる
fs.clear();//なんとなく
fs.open(file.c_str(), ios::binary | ios::out); //ファイルに書きこむ用意
//リプレイ情報を取得する
fs.write( (char *)&replayP, sizeof(CreplayPractice) ); //リプレイ情報を書き込む
char buf[16];
while( !ifs.eof() ) //ファイルのコピーでリプレイのキー入力を書き込む
{
ifs.read(buf,sizeof(buf));
// ファイルの終端ではなくエラーが出たらとりあえず抜ける
if ( ifs.fail() && !ifs.eof() )
{
mojiwindow.call("ファイルコピー中にエラー");
break;
}
fs.write(buf,ifs.gcount());
}
break;
}
fs.close(); //一度閉じる
fs.clear();//なんとなく
}
}
//オマケ
void setreplayseed(int seed)
{
replayP.replaybase.setseed(seed);
}
int getreplayseed()
{
return replayP.replaybase.getseed();
}