どこでも……? パイズリ……? システム……?
これのことです。
要は常にヒロインが後ろからついてきて話しかけるとどこでもパイズリができるというそのままのシステムなのですが、意外とそれっぽいシステムのゲームを見ません。
それこそ私の過去作『黒先輩』くらいなものでしょうか。
わりと汎用性の高いシステムな気がするので実装方法について共有しておきます。
※以下の記事は中~上級者向けです。
プラグイン? オーバーライド? と聞いてわからない方にはあまりオススメできません。
・フォロワーに独立してもらおう
似たようなことは過去作『魔機人形』でやったものです。
本作はそのマイナーチェンジ版といえます。
後輩にせよ魔機人形にせよ、ついてくるこいつらはなにものなのか?
こいつらです。
こいつらのことを「フォロワー」と呼びます。
デフォルトではただプレイヤーの後ろをついてくるだけの存在ですが、本作ではこのフォロワーを独立させ、当たり判定をつけて機能させています。
まずは独立させる方法です。
プレイヤーに追従して動く処理がありますので、これを消しましょう。
この処理はGame_FollowersのupdateMoveで各フォロワーごとにchaseCharacterが実行されることで動いてます。
(Game_FollowersとはGame_Follower=個々のフォロワーを格納しているクラスのことです)
ので、以下のようにオーバーライドするとよいでしょう。
Game_Followers.prototype.updateMove = function() {
};
Game_Follower.prototype.chaseCharacter = function(character) {
};
要らない処理なので空白にしてしまうわけです。
Game_Followers.prototype.update = function() {
/*if (this.areGathering()) {
if (!this.areMoving()) {
this.updateMove();
}
if (this.areGathered()) {
this._gathering = false;
}
}*/
for (const follower of this._data) {
follower.update();
}
};
あるいはこうでもよいです。
プレイヤーの後ろをついてくる処理をコメントアウトしています。
さて、以上を適用してゲームを実行するとどうなるでしょう?
見事フォロワーを置き去りにできました。
・フォロワーに動いてもらおう
さて、動かなくなったので今度は代わりに別の動きをしてもらう処理を書きます。
ツクールにおいてリアルタイムの処理はupdateという関数でなされています。
この関数は毎フレーム、つまり1秒に60回呼び出されるものです。
では、Game_Followerのupdateにはなにが書かれているでしょう。
Game_Follower.prototype.update = function() {
Game_Character.prototype.update.call(this);
this.setMoveSpeed($gamePlayer.realMoveSpeed());
this.setOpacity($gamePlayer.opacity());
this.setBlendMode($gamePlayer.blendMode());
this.setWalkAnime($gamePlayer.hasWalkAnime());
this.setStepAnime($gamePlayer.hasStepAnime());
this.setDirectionFix($gamePlayer.isDirectionFixed());
this.setTransparent($gamePlayer.isTransparent());
};
いろいろ書かれていますが……
要するにこれは全部「プレイヤーに従え」という処理です。
いけません。まだプレイヤーから独立できていないようです。消しましょう。
Game_Follower.prototype.update = function() {
Game_Character.prototype.update.call(this);
this.setTransparent($gamePlayer.isTransparent());
this.updateDashing();
this.updateAction();
};
こうなります。
ちなみにsetTransparentは透明設定で、これだけは残しておきます。
プレイヤーが透明の場合はフォロワーが透明でだいたい問題ないからです。
(もし問題がある場合はこの行も消す)
さて、唐突に表れたupdateDashingとupdateActionですが、これは今から書きます。
updateDashingは本来Game_Playerのみが持つ関数で、文字通り「ダッシュしているかどうか」を判定する関数です。
updateActionはまったくの独自定義関数です。重要なのはこちらです。
まずは簡単に「プレイヤーの方を向く」という処理を書いてみます。
Game_Follower.prototype.updateDashing = function() {
};
Game_Follower.prototype.updateAction = function() {
this.turnTowardCharacter($gamePlayer);
};
updateDashingの方はひとまず仮に空のものを置いておきます。
これでプレイヤーの方を向く動きができました。
あとはこの応用です。
そうですね、次はランダムに動かしてみましょう。
Game_Follower.prototype.updateAction = function() {
this.moveRandom();
};
これでフォロワーは好き勝手に動き回るはずです。
いくらなんでも好き勝手すぎる。
どうしてこんなことに?
updateは毎フレーム、つまり1秒間に60回呼び出される関数です。
1秒に60回もランダムに動けばこうなってしまうのは当然の結果です。
よって、行動回数を抑制する処理を書かねばなりません。
そうですね、動くのは1秒に1回くらいで十分でしょう。
つまり60フレームに一回動けばいいわけです。
Game_Follower.prototype.updateAction = function() {
if(this._waitCount > 0){
this._waitCount--;
return;
}
this.moveRandom();
this._waitCount = 60;
};
つまりこうです。
今度はまともな動きになりました。
障害物は平気で無視してますが、これはまだフォロワーに当たり判定を実装していないからです。
動くようになると気になりますので、次は当たり判定を実装してみます。
・フォロワーを実体化させよう
フォロワーには当たり判定がありません。
壁も平気ですり抜けるし、プレイヤーとも触れ合えません。なぜでしょう?
①すり抜けフラグがtrueになっているから
②衝突判定がそもそも実装されていないから
答えは両方です。
まずは①から見てみます。
Game_Follower.prototype.initialize = function(memberIndex) {
Game_Character.prototype.initialize.call(this);
this._memberIndex = memberIndex;
this.setTransparent($dataSystem.optTransparent);
this.setThrough(true);
};
Game_Followerのinitialize(初期化)関数です。
ここにあまりにもそれっぽい記述があります。
this.setThrough(true)です。これを消しましょう。
プレイヤーやお互いのことは無視しますが、マップ上の障害物にはちゃんとぶつかってるのがわかると思います。
つまり、キャラクター同士の衝突判定とマップの衝突判定は別物だということです。
次は②のキャラクター同士の衝突判定を実装します。
Game_CharacterBase.prototype.isCollidedWithCharacters = function(x, y) {
return this.isCollidedWithEvents(x, y) || this.isCollidedWithVehicles(x, y) || this.isCollidedWithFollowers(x,y);
};
Game_CharacterBase.prototype.isCollidedWithFollowers = function(x, y) {
const followers = $gamePlayer.followersXyNt(x, y);
return followers.some(function(follower) {
return follower.isVisible();
});
};
Game_Player.prototype.followersXyNt = function(x, y) {
var followers = this.followers().visibleFollowers();
return followers.filter(function(follower) {
return follower.posNt(x, y);
});
};
もとからある衝突判定をもとにフォロワーにも適用させたものです。
この処理を書くとどうなるか?
プレイヤーがフォロワーを通り抜けできなくなります。
ですが、フォロワーの方はプレイヤーを通り抜けます。
衝突判定はそれぞれのキャラクターごとに設定しなければならないのです。
これはGame_Eventを参考にしましょう。
Game_Follower.prototype.isCollidedWithCharacters = function(x, y) {
return (Game_Character.prototype.isCollidedWithCharacters.call(this, x, y) ||
this.isCollidedWithPlayerCharacters(x, y));
};
Game_Follower.prototype.isCollidedWithPlayerCharacters = function(x, y) {
return this.isVisible() && $gamePlayer.isCollided(x, y);
};
つまりこうです。
これでフォロワーが実体を得ることができました。
・フォロワーに話しかけられるようにしよう
せっかく実体化したので話しかける処理を入れましょう。
これもGame_Eventが参考になります。
どちらかというとプレイヤーが主体となる処理なのでGame_Playerの方に書きます。
Game_Player.prototype.triggerButtonAction = function() {
if (Input.isTriggered("ok")) {
if (this.getOnOffVehicle()) {
return true;
}
this.checkEventTriggerHere([0]);
if ($gameMap.setupStartingEvent()) {
return true;
}
this.checkEventTriggerThere([0, 1, 2]);
if ($gameMap.setupStartingEvent()) {
return true;
}
}
return false;
};
プレイヤーがイベントを調べるときの関数はこのへんです。
近い処理はcheckEventTriggerThereです。
これを参考にcheckFollowersTriggerThereという関数を書いて挿入してみます。
Game_Player.prototype.triggerButtonAction = function() {
if (Input.isTriggered("ok")) {
if (this.getOnOffVehicle()) {
return true;
}
if(this.checkFollowersTriggerThere()){
return true;
}
this.checkEventTriggerHere([0]);
if ($gameMap.setupStartingEvent()) {
return true;
}
this.checkEventTriggerThere([0, 1, 2]);
if ($gameMap.setupStartingEvent()) {
return true;
}
}
return false;
};
Game_Player.prototype.checkFollowersTriggerThere = function() {
const direction = this.direction();
const x1 = this.x;
const y1 = this.y;
const x2 = $gameMap.roundXWithDirection(x1, direction);
const y2 = $gameMap.roundYWithDirection(y1, direction);
this.followers().forEach(function(follower) {
if(follower.isVisible() && follower.posNt(x2,y2)) {
follower.talkPlayer();
return true;
}
}, this);
};
Game_Followers.prototype.forEach = function(callback, thisObject) {
this._data.forEach(callback, thisObject);
};
Game_Follower.prototype.talkPlayer = function() {
this.turnTowardPlayer();
};
待ってくれたまえ。コードの洪水をワッといっきにあびせかけるのは!
なにはともあれ、これで話しかけるとこっちを向く処理まではできました。
先にも書いた通りcheckFollowersTriggerThereという関数の中身はcheckEventTriggerThereを参考にしたものです。
プレイヤーの現在位置と向きをもとに、その先にフォロワーがいるかどうかを判定しています。
決定キーを押したときフォロワーがいたのなら話しかけたという処理が成立し、talkPlayerという関数が呼び出されます。
talkPlayerの中身は現在は「プレイヤーの方を向く」だけです。
this.followers().forEach~のあたりは、フォロワーは複数いるのでfor文で回してすべてのフォロワーについて確認しているということです。
さて、ここまでできたらtalkPlayerの中身を書き換えるだけで話しかけるという処理は完成します。
コモンイベントでも呼び出しましょう。
Game_Follower.prototype.talkPlayer = function() {
this.turnTowardPlayer();
this._waitCount = 60;
$gameTemp.reserveCommonEvent(1);
};
ただ、これではすべてのフォロワーで同じコモンイベントが呼び出されることになります。
キャラによって別のコモンイベントを呼び出したい場合はたとえば以下のように書きます。
Game_Follower.prototype.talkPlayer = function() {
this.turnTowardPlayer();
this._waitCount = 60;
const actorId = this.actor().actorId();
$gameTemp.reserveCommonEvent(actorId);
};
これでアクターIDと同じ番号のコモンイベントが呼び出されます。
こんな感じにコモンイベントを組んでおきます。
さて、実行してみましょう。
おお……これはもう、完成なのでは……??
・フォロワーが邪魔!!
ただ、このシステムには問題があります。
フォロワーが実体をもって動き回るせいで邪魔なのです。
ふつうのRPGでも町の人が邪魔をして通れなくて困る経験はあると思います。
フォロワーはそれが常について回るのです。どうすればいいでしょう?
解決手法はほかにもあるかもしれませんが、一定時間以上ぶつかり続けると場所を交換する処理を組みました。
これもプレイヤーが主体となる処理です。Game_Playerに記述します。
checkEventTriggerTouch関数のなかに入れ込んでしまいましょう。
const _Game_Player_checkEventTriggerTouch = Game_Player.prototype.checkEventTriggerTouch;
Game_Player.prototype.checkEventTriggerTouch = function(x, y) {
_Game_Player_checkEventTriggerTouch.apply(this, arguments);
this.checkFollowersTouching(x, y);
};
Game_Player.prototype.checkFollowersTouching = function(x, y) {
this.followers().forEach(function(follower) {
if(follower.posNt(x,y)){
follower.turnTowardPlayer();
follower.countObstacle();
if(this.isDashing()) follower.countObstacle();
}else{
follower.resetObstacle();
}
if(follower.obstacle() > 20) {
this.swapFollower(follower);
}
}, this);
};
まずこれは20フレームぶつかり続けるとswapFollowerが起こるという処理です。
countObstacleはフレームをカウントする関数です。
resetObstacleはカウントをリセットする関数です。
以下のように書きます
Game_Follower.prototype.countObstacle = function() {
this._obstacle++;
};
Game_Follower.prototype.resetObstacle = function() {
this._obstacle = 0;
};
Game_Follower.prototype.obstacle = function() {
return this._obstacle;
};
swapFollowerはjumpでプレイヤーとフォロワーの位置を交換する処理です。
Game_Player.prototype.swapFollower = function(follower) {
this.jumpStraight();
follower.turnTowardPlayer();
follower.jumpStraight();
follower.resetObstacle();
};
Game_CharacterBase.prototype.jumpStraight = function() {
var x = 0;
var y = 0;
const d = this.direction();
if(d == 8){
y = -1;
}else if(d == 6){
x = 1;
}else if(d == 4){
x = -1;
}else if(d == 2){
y = 1;
}
this.jump(x,y);
};
ここで注意ですが、ツクールはデフォだとプレイヤーがジャンプするとフォロワーも一緒にジャンプする仕様になってます。フォロワーに自我はないのか。
ので、その処理を無効にする必要があります。
Game_Player.prototype.jump = function(xPlus, yPlus) {
Game_Character.prototype.jump.call(this, xPlus, yPlus);
//this._followers.jumpAll();
};
this._followers.jumpAll();をコメントアウトしましょう。
これで邪魔なフォロワーと立ち位置を交換する処理が書けました。やったね。
・フォロワーについてきてもらおう
肝心なことを忘れていました。
今のままではフォロワーとは名ばかりのただランダムに動き回るだけの存在です。
よって、とりあえずランダムに動くだけの処理を書いていたupdateActionを改良します。
また、主人公を追いかける処理ですがツクールにはデフォで経路探索の関数が用意されています。
クリック操作によるプレイヤー移動でクリック先に向かう動きに使われています。
これを利用させてもらいましょう。
というより、ツクールMVのプラグインであるSmartPathをちょっと改造させてもらって使います。
Game_CharacterBase.prototype.setTarget = function(target, targetX, targetY) {
this._target = target;
if (this._target) {
this.setTargetXy();
} else {
this._targetX = targetX;
this._targetY = targetY;
}
};
これは向かう先、つまり文字通りターゲットをセットする関数です。
第一引数のtargetにはcharacter、第二・第三引数は座標をセットします。
フォロワーの向かう先はプレイヤーですので$gamePlayerを入れればよい、はずですが……
実は、これには問題があります。
フォロワーは$gamePlayerの中に含まれているクラスなのです。
つまり無限ループ構造が発生します。
MVのころはこれで問題なかったのですが、JSON変換かなんかの仕様が変わった影響で、MZだとセーブができなくなるのです。
よって、苦肉の策ですが以下のようにします。
Game_CharacterBase.prototype.setTargetXy = function() {
if(this._target === 'player'){
this._targetX = $gamePlayer.x;
this._targetY = $gamePlayer.y;
}else{
this._targetX = this._target.x;
this._targetY = this._target.y;
}
};
プレイヤーを追いかけたい場合は$gamePlayerではなく"player"と文字列を入れることにします。
Game_CharacterBase.prototype.clearTarget = function() {
this._target = null;
this._targetX = null;
this._targetY = null;
};
さらにターゲット関連の変数を初期化する関数もつくり……
const _Game_CharacterBase_updateStop = Game_CharacterBase.prototype.updateStop;
Game_CharacterBase.prototype.updateStop = function() {
_Game_CharacterBase_updateStop.call(this);
if (this._target) {
this.setTargetXy();
}
if (this._targetX != null) {
direction = this.findDirectionTo(this._targetX, this._targetY);
if (direction > 0){
this.moveStraight(direction);
}
}
};
updateStopという関数にエイリアスで書き加えることで経路探索で追いかける処理をつくります。
findDirectionToという関数がそれです。
それなりに精度の高い経路探索ですので、殺人ピエロが追いかけてくるみたいな動作にも使えます。
さて、準備ができましたのでフォロワーに動きを組み込みましょう。
そうですね、3タイル以上離れたら追いかけフラグが立つ仕様にしましょう。
新たにchasePlayerという関数を用意します。
Game_Follower.prototype.updateAction = function() {
if(this._waitCount > 0){
this._waitCount--;
return;
}
this.chasePlayer();
};
Game_Follower.prototype.chasePlayer = function() {
// ついていく
if(this._follow && this._waitCount <= 0){
this.setTarget('player');
}
// 一定以上離れた
if(!this._follow && this.distance($gamePlayer) > 3){
this._follow = true;
this._waitCount = 60;
}
};
距離を測るためのdistanceという関数も独自定義のものです。
以下のように書きます。
Game_CharacterBase.prototype.distance = function(target) {
const sx = this.deltaXFrom(target.x);
const sy = this.deltaYFrom(target.y);
const ax = Math.abs(sx);
const ay = Math.abs(sy);
return Math.sqrt(ax**2 + ay**2);;
};
ついてくるようになりました。モテモテですね。
さて、これをもう少し改良して離れすぎるとダッシュで近づいてきたり、暇なときはブラブラしたりする動きを加えてみましょう。
Game_Follower.prototype.chasePlayer = function() {
// ついていく
if(this._follow && this._waitCount <= 0){
this.setTarget('player');
}
// 暇
if(!this._follow && this.distance($gamePlayer) > 1){
this.moveRandom();
this._dashing = false;
this._waitCount = 60;
}
// 一定以上離れた
if(!this._follow && this.distance($gamePlayer) > 3){
this._follow = true;
this._waitCount = 60;
}
if(this.distance($gamePlayer) > 6){
this._follow = true;
this._dashing = true;
this._waitCount = 0;
}
// 隣接
if(this.distance($gamePlayer) <= 1){
this._follow = false;
this._dashing = false;
this._waitCount = 60;
this.clearTarget();
}
};
ちょっと長い関数になってしまいました。
また、ダッシュしてるかどうかを示す関数の定義も行います。
Game_Follower.prototype.isDashing = function() {
return this._dashing && !$gameMap.isEventRunning() && !$gameMap.isDashDisabled();
};
this._dashingはそのままダッシュさせたいときにtureにする変数です。
あとはイベント中はダッシュしない、ダッシュ禁止マップではダッシュしないと書いてあります。
どういう動きになるでしょうか。
わんこみたいで可愛いですね。
・一応用意しておきたい、緊急ワープ
馬鹿な!? さすがにもう完成なのでは……
ほとんど問題ないといえばないのですが、経路探索も完璧ではありません。
あまりに入り組んでいると、どうしてもプレイヤーの場所まで辿り着けない場合も出てきます。
このように極端に複雑なマップだと経路探索も振り切れてしまいます。
こうなってしまうと、話しかけたい仲間がそばにいなくて面倒ですよね。
よって、緊急ワープを実装します。
と、コードを引用しようと思ったら長い長い……。
urgencyWarpという関数がそうなのですが、えっと、最後にプラグインを配布しますのでそこで確認ください。
簡単に紹介しますと、プレイヤーの周囲8座標を調べて通行可能かどうかを判定し、可能ならその場所に飛ぶ、というものです。
通行可能かどうかの判定というのは、たとえば「壁の中にいる」を防止するための措置です。
考え方としましては、あまりに長い間プレイヤーのもとに辿り着けないとurgencyWarpが発動する、というものです。
これでいつまでもいっしょだね。
・そしてどこでもパイズリへ――
以上の処理が完成すれば、あとはコモンイベントの内容をあれこれするだけでこうなります。
改めて一から紹介してみると結構めんどくさいことしてますね。
さらには、このシステムからどこでもパイズリに至るには――
現在位置の情報からふさわしいパイズリを判定したり、会話内容を状況に応じて変えたりと、他にもいろいろあります。
IndependentFollower.js (15.61kB)
ダウンロード
というわけで、今回の内容をまとめてプラグイン化したものがこちらです。
これを配布してくれるならこれまでの講義はいったい?! という感はありますが、実際に使おうとした場合はいくらか手直ししたくなる部分が出てくるかと思います。
そのためのリファレンス代わりとしてこの記事はご利用ください。
本プラグインを導入される場合にはツクールMZの公式プラグインPluginCommonBaseとPluginBaseFunctionが必要です。
また、記事内容と異なる点としてはトークイベントの指定法をより汎用性の高いものにしています。
このようにメモ欄に記述します。
さて、これで技術的な準備は整いました。
どこでもパイズリシステムはもう目前です。さあ、歩み出しましょう。