神無月サスケの波瀾万丈な日常

神無月サスケのツイッター(@ktakaki00)を補完する長文を書きます。

続・困った友人:本を読まない人が自分の直感に身を委ねてはいけない理由

先日話した(d:id:ktakaki:20120504:p1)、困った友人について。

最近彼のツイッターを久しぶりに見ると、さらに暴走していた。彼のフォロワーさんたちは彼のツイートを観て一体どう思っているのだろうか、心配だ。

彼はツイートで、自分と相容れない考えを「過激だ」「もっと考えることがあるんじゃない?」と批判し始めている。しかもこれは「自分で考えることが大切だと目覚めた」とあるから自分で考えた結果らしい。これだけだと分かりづらいだろうが(ツイート内容の詳細を書くと個人が特定できる恐れがあるので避ける)以前の彼の主張と矛盾しているばかりか、現状の彼自身の自己批判のようにさえ見える。なによりその一連のツイートの意見自体に一貫性を感じず、それ自体が矛盾しているように感じた。

なんで彼はこんなおかしな事を言い出したのか。

恐らく彼は今、自分の目から鱗が落ちたと思っているのだろう。しかし、目から鱗が落ちたと思っていたら、実は目に鱗が入っていた、ということが多々ある。今の彼は、まさにその状況だ。誰か周囲に注意してくれる人がいれば、そうはならずに済んだのだろうが、彼はそういう話が出来る友人とことごとく縁を切ったらしく、どんどん暴走している。

こうなった理由の一つは、普段彼が本を読まないからだと感じた。ノンフィクションで小説でもいいから、普段から本を読んでいれば、仮に歴史や政治経済などの専門分野の「知識」には明るくなくても、そこにある「人間心理」の部分に対する考察はかなり豊かになる。そして、人間心理に対する考え方を理解すれば、歴史や政治経済も人間の作るものであるため、根底に流れる部分は理解できる。つまり、人間性を豊かにしたければ、本は読むべきなのだ。しかし、彼は本を読まないため、「人間心理に対する考え方」が貧しく感じた。

また、彼は、本や他人の意見に耳を傾けないため、幅広い物の考え方が出来ない。通常、対立する意見がある問題に対して、どちらかの意見を支持する場合、冷静な人なら、自分の賛成する意見と反対する意見を見たで熟考して、その結果、自分の支持する側を確定し、理解を深めていくものである。しかしそうでない人は、自分の感情ありきで、一方的なものの見方をして決め付け、他人の意見を聞いたり、そういう本を読んだりしない。そういう危うい人は、あるとき何かの拍子に考えがひっくり返ると、オセロの白が黒にひっくり返るように、途端に正反対のことを言い出すのである。本を読まない人は、幅広い意見を許容しない人であり、非常に危うい。彼がまさにそういう「危うい人」である。

本を読まず、人の意見にも耳を貸さず、層の厚い考察が出来ない、そういう「危うい人」が自分の意思決定の礎とするのは「自分の直感」だけになる。

彼は以前から、頑なに「自分の直感」を尊いものだと信じていた。彼はよく口にしていた、「人間の思考エンジンには2つあり、一つは頭、一つは心である。頭が混乱したら、心に向けてみよう」と。これはある部分では正しい。頭は論理的に物を考えるから、複雑な問題がどっと入ってくると一気に整理が出来なくなる。一方心はこれまでの経験を非論理的な形で整理できるから、心に頼ったほうが、思わぬ解決策が生まれることがあるのだ。この「頭と心」、人によっては左脳と右脳という人もいるかもしれない。今の彼は感じているのだろう、理屈で考えても仕方が無い、と「自分の直感」だけで動き、物を言うべきだ、と。

しかし、である。直感とは、それまでの人生経験および積み上げた知識が物をいう。その知識の中には、まさに本で読んで得た「人間心理に対する考え方」が含まれる。本を読まない人でも、人生について深く語り合う友人がいれば、その人と語り合うことでそれを得られる。しかし彼はそういう人達を「耳障りなことを言う」と切ってしまった。そういう人は、自分の経験だけが頼りになってしまう。しかしせいぜい20〜30年生きている人の人生経験なんてたかが知れている。世界中を旅するとか起業するとか、そういう一味違った経験をしている人ならもちろん話は別だが、そういう人ほど、多くの本を読み、多くの人に会っているものだ。そうでない凡人の、本を読まずに人にも相談せずに、自分自身の経験だけで出てきた直感なんて、大したものではないのだ。

本当に直感を働かせたいのなら、もっと本を読むべきだし、様々な人と深いレベルで話をするべきだし、いろんな意見に耳を傾けて検討すべきなのだ。彼にはそれらが圧倒的に足りない。そのため、今の彼は非常に危うい。

果たして今後、彼はどうなっていくのだろうか。何か言ってやりたいが、僕は彼から逆上されて一方的に関係を切られた身。いちいち口を出す義理もない。結論を出せないまま、今日はここまで。

ツイッターで自分の主義主張に合わない意見を見かけたときの対処法

最近のツイッターの流れを見ていて、少し感じることがあったので、少し長くなるけど書きます。通し番号をつけますので順番に読んでください。

ツイッターに書くつもりでしたが、長くなったのでブログに書きました。全てのパラグラフが140文字以内なのは、そのためです。

第一部:総論:ツイッターでの感情的な議論が不毛な理由

1)よくツイッターで議論をしている人がいるが、それで相手の考えを変えることは無理だと悟るべきだ。朝まで生テレビなんかを観ていれば分かるけど、反論されれば相手は反感を示して余計に意地になるだけ。声が大きい人が勝者。最後はブロックしあって終了。不毛だ。

2)ツイッター上で主義主張の異なる相手を黙らせたり、考えを変えさせたりするのは無理と気付くべき。そのテーマに詳しくない人を洗脳するくらいは出来ても、事実に基づく説得でないなら、その洗脳はすぐ覚める。唯一賢明なのは、誠心誠意事実を話し、理解者を勝ち取っていくことだけだ。

3)本当に相手の考えを変えたいなら、「自身に都合のいい理屈」ではなく「事実」を示すこと。仮に貴方にとって「事実」が都合が悪いので相手をごまかすつもりなら、貴方の方が変わるべき。その上で、仮に相手が自分の結論ありきで貴方の意見を切り捨てたら、彼らはそのうち自滅するから無視してよい。

4)世間では大抵、事実に基づいた意見と、利権に目のくらんだ意見が、一番広まっていくものと相場は決まっている。後者に流されなくないなら、反対意見もよく聞いて、自分で判断するしかない。自分の感情ありきで、反対者を「カルト」や「思考停止」などと誹謗している人の意見が一番聞くに値しない。

5)「ペンは剣より強い」この言葉は、十年単位で見れば真実でないこともある。しかし百年単位で見れば、真実であることは歴史が証明していると感じる。本当に正しいことは発言し続ければ、いつか世間に認められる。貴方の意見は百年単位で主張に堪える物か、胸に手を当て考えるべきだ。(続く)

以上、総論終わり。まだ続きますが、この後は僕の個人的な話題になるので興味がある方のみお付き合い下さい。

第二部:主義主張に我慢が出来ない意見を言う人への実際的な対処法

6)僕がツイッター上で議論する気がないのは、パソコン通信時代の経験による。議論に勝ち負けをつけることは出来ても、負けた側の意見は変えられないと悟った。議論好きは、勝ち負けだけに満足する人が多すぎる。より酷いのは、相手の立場を考えず一方的に主張だけする人間。もううんざり。

7)よく相手を批判する際に「お前は感情的だ」「思考停止」などと言う人がいる。そこで思い出すのが、パソコン通信時代の議論。きちんと相手の意見を聞く不文律があって、その前提で行けば、相手の意見を思考停止などと言うのは反則負けである。大抵思考停止しているのは、この言葉を使った方だ。

8)では、ツイッター上で自分と相反する意見を見かけた時、どうすべきか。僕は反対意見であっても興味深ければツイートを追うが、結論ありきの意見を持つ人には失望し、リムーブする場合もある。議論しても彼らの意見を変えるのは無理だから、嫌なら自分から距離を置くしかない。

9)リムーブされて怒る人はいるだろう。対等に議論しろという人もいるだろう。しかしそれは、相手に自分の土俵に引きずり込むまれろと言っている。傲慢と気付くべきだ。リムーブは、貴方が言いたいことを言った上での相手の反応である。議論に応じるかどうかは、リムーブした側の裁量と理解すべきだ。

10)仮に主義主張の対立が原因で相手にリムーブかブロックされた場合、貴方に出来るのは、自分のTLで自分の主張をすることしかないと気付くべきだ。もちろん、相手にリムーブやブロックを返すのは自由だ。ツイッター上の議論で相手の意見は変えられないと、早く悟るべきなのだ。

11)結局、ツイッターは議論ではなく、主張と意見共有のツールなのだ。貴方が支持される意見を言えばリツイートされて広まるし、仮に貴方が支持できない意見を見たら、反論の代わりに、自分が正しいと思うことを言えばいい。それが皆に支持されれば反対意見を覆せる。そういうことだ。(終)

結論

以上がツイートする予定だった11のパラグラフである。

結果的にであるが「ツイッター上での議論は不毛」ということを敢えてツイッターでは書かずにブログで書けたことは、良かった気がする。ここなら、お互い疲れない範囲であれば、議論してもいいかな、生産的な議論が出来そうだと思わせてくれる、それがツイッターと違う、ブログという場所の「温度差」なのかな、と思った。

また、はてブで共有される可能性があり、そうなれば賛否両論あるだろう。でも僕はそういうはてブという場所が好きだ。そこでの意見はいろいろ参考にしたい。

追記:絡まれたり、親しい人に反論された場合の対処法 (22:15 2012/05/07)

このエントリは、これまで数回、知り合いからメールで「ツイッターで困ってます」という相談を受けたため、返信した内容である。同じことを感じている人も多いと思い公開した次第である。

そして僕に相談してきた人は他の問題も相談していたのを思い出した。こちらの回答も共有の価値があると思い、僕の意見を追記する。

見知らぬ人から絡まれたくない場合の対処法

補1)ツイッター上で自分の意見を書いた結果、反対意見を持つ者に突然絡まれることがある。しかもそういう輩の多くは、結論ありきで物を言い、相手と話し合う気はなく、自分の意見を押し付ける事しか考えない。彼らを避けるために発言を控えるとしてもなお、消極的な意思表示方法がある。

補2)直接的に意見を述べずとも、支持意見をfavしたり支持者をフォローしたりすれば、相手には貴方の支持が伝わる。そのことによって、貴方にも自信が付く。批判者は大抵わざわざfavやフォロワーまで見ないし、仮にそこを批判されたら「favやフォローは必ずしも同意ではない」と言えばいい。

仮にそれでもこういう輩に悩まされたらブロックが有効なのは言うまでもない。

親しい人との論争を避けるための対処法

「話が通じない相手にはリムーブまたはブロック」と前述したが、今度は「話が通じる相手」の場合を述べる。実はこれが一番厄介だ。

補3)自己主張をした結果、これまで貴方と親しかったフォロワーが反感を持ち、関係がこじれる場合がある。貴方がそういう主張を見かけて不快になることもある。これは意外と痛い。特に趣味で繋がっていた人で、普段その趣味の話しかしない人が、突然ある時思想信条の話を始める時は、大抵地雷である。

補4)もし貴方がその問題に対して、賛否両論を吟味した上で意見を持っているなら、相手の発言の経緯が分かるはずだ。仮に相手も吟味した上での反対意見だと分かったら、貴方も自然と納得できるはず。問題は、相手が勉強不足で、不用意に持論を展開した場合。これは不快になって当然。

補5)この場合、本当に親しい相手ならさりげなく忠告出来る。聞いてくれるだろう。問題は、それほど親しくないがツイッター上だけで親しい相手である。さりげない忠告が批判と受け止められ、関係が悪化する可能性がある。一度くらいは大目に見るのも手だが、他に有効な方法はある。

補6)決定打とは言えないが、空中リプライ(相手の意見の返答にあたる文章を、返答ではない形で書くこと)が有効だ。それも空中リプライと気づかれない形でやること。相手が読めば、自分の考えを改めるきっかけになる。直接言うと角が立つが「自発的に気づきを促す」という形なら受け入れられ易い。

補7)それでも我慢が無理そうなら、最後の手段は「そっと」リムーブすること。相手もそれに気づくため、リムーブで返される覚悟も必要だが、そこは割り切るしかない。しかし仮にリムーブされた理由を聞かれたら、チャンスである。相手が話を聞く姿勢なので、意見の相違を述べ話し合えばよい。(終)

大切なのは、話し合うことが出来る場合は(これは少ないチャンス)、貴方が深呼吸して冷静に、相手を尊重して話すこと。

また、上記の他にミュート機能のあるクライアントを使うのも手だが、これは現状打破にならないと思い取り上げなかった。これでいいという人は検討してほしい。

困った友人のこと

僕の困った友人の話を書きます。本当はツイッターに書こうと思いましたが、あまりに長くなりすぎたので、ブログに書きました。僕のブログにしてはやや口語的で、1パラグラフが全て140文字以内に分けられているのはそのためです:

僕には親しい友達がいる。正式にはいた、という方がいいかもしれない。彼は家族に内緒で高額の借金をしてきたし、散財が凄かった。毎年4回ほど旅行に行き、その度に高額なものを買っていたし、日常でもやたら高級品を買っていたようだ。

僕は彼とは長い付き合いだった。最近も電話で相談を受けていた。しかし、結構彼の態度には気になる部分があった。それで今年の頭、彼に「最近ちょっとたるんでいないか?」と指摘したが、それで逆上され、一方的に関係を切られてしまった。

彼は、リボ払いのクレジットカードで買い物を続け百万近い債務を背負った挙句税金関係(市民税や保険料など)のお金もずっと滞納していた状態だった。税金関係は家族会議(後述するが僕が提案した)で言われてようやく分割で少しずつ支払いを始めたらしい。

僕は放置して自滅させる選択肢もあった。しかしお金の件は彼の家族にも迷惑がかかる。彼の家族にはお世話になった。それで、家族にもこのことを電話で相談した。お母様は、弟や妹達はしっかりしているのに彼だけ……とかなり心配していた様子だった。

僕が彼の借金に対して黙ってきたのは、借金がこんな額なんて知らなかったことと、彼自身にもその程度の節度はあるだろうと信じてきたからだった。何より、彼に夢を目指して欲しかったからお金の話は二の次だった。彼に縁を切られ、彼の両親に相談し、初めて彼の借金の額が分かってびっくりした。

僕は、彼にはしっかり夢を目指して欲しいと思い、そのように助言してきたつもりだった。正社員になるかどうか悩んだときに、夢があるなら若いうちに目指すべき、と言った(今ではそのことに責任を感じている)。しかし彼は、一向に夢に向けた努力はしなかった。仕事もふらふらと派遣を転々としている。

僕の周りで、夢を目指し、それで食べようとしている人がいるが、皆、芯が通っている。「駄目だったら溶接の仕事につく」と言っている人もいるが、夢を目指した経験のある人はタフなので、仮に夢で食べていけなくても、結構、何とかなる。しかし、夢を目指さずふらふらしていた人は違う。

彼は、このGWに東北に旅行に行ってきたようだ。結構散財したようだ。「東北にお金を落とすのも目的」と話していたようだ。考えとしては立派だが、そういうのはまず、自分の懐具合を勘案してするべきものではないか。以前は僕が彼の行きすぎを指摘していた。誰も指摘しない今は危ういと思った。

彼は、家族の言うことを聞かない。自分からそう話していた。よく、家族からたまに思いついたように「ちゃんとした仕事見つけろ」と言われているが、そんなものは聞き流している、と。だから僕はご両親に、家族会議を開く形できちんと問い詰めることを提案した。

彼には自分が本当にやりたいことが何か見つけて欲しいし、さもなくば正社員になるなりして身を固めて欲しいと思う。彼はいつもやりたいことがあると言うが、大抵3ヶ月すると違うことを言い出す。前は「尾道は良かったので住みたい」と言い、3ヶ月経つと「やっぱり京都で仕事を探したい」と言ったり。

仮に本当に京都で働きたいのなら、身体一つで飛び込んで、住み込みで働かせてくれる旅館なんて探せばいくらでもあるだろう。なぜすぐに動けないのか。……電話で両親にそう話すと「彼は人付き合いが苦手だから、接客業や、中間管理職はつとまらない」と話してくれて、少し腑に落ちた。

彼は僕と同じように発達障害の部分があったんだろうね。僕の方は診断を受けてる症状なので気付かなかったが。発達障害の人って、二次障害が出やすい。躁状態になって散財をするのも、対人面の仕事で鬱になるのも、そうなんだろうな、と思う。

彼はうつ病と診断されているようだが、言動を見ていると、それは誤診だ。突然気が大きくなる、躁状態が出るのは、双極性障害である。これらは薬も治療方法も全然違うし、精神科の先生も誤診しやすいため、一度自己申告した方がいいと思っているんだが。

彼のことは、共通の友人にも相談して、どうするか考えている。彼には早く本当にやりたいことを見つける努力をすることが必要だし、その夢の実現のために、いろんな欲望を我慢する必要もあるだろう。借金は一番に何とかしないと……話し合っている。

そもそも昔の彼はこんなんではなかった。正義感もあるし、夢も持っていて、人当たりもよかった。そんな彼が、明らかに劣化して、「かっこ悪くなって」いると感じた。夢に敗れたのか、仕事に失望したのか、彼の持っている利点を自らことごとく棄ててしまったように僕には思えた。

僕はそのことを指摘して、長年の関係を切られてしまった。僕は歯に衣着せないものの言い方をするため、僕の言い方が悪かったのだろう。そこは申し訳なく思う。ただ、僕はまだ彼を信じている。僕と仲直りはしなくてもいいから、前のようにちゃんとして、周囲に迷惑をかけない人になってほしい。

彼は過去は本当にこんな人ではなかった。ただ少し暴走しがちなところはあったが。夢に溢れていて、貪欲にチャレンジする人だった。暴走しがちな部分については、僕がセーブさせることで、いい塩梅にしてきた自負はある。もう、あの頃のように彼と夢を語り合うことは出来ないのだろうか。

彼は本当に根はいい人だと思う。歳を経て、出来ることと出来ないことが分かって来たんだと思う。僕も同じような経験をして、今は無理のない生活をしている。だから彼にも今こそ、自分を見つめなおして出来ることをして欲しいと思う。世の中に失望したなどと言って安易な道を選ばないで欲しい。

最後に。長年付き合ってきた僕を一方的に切った貴方へ。崖に向かって一直線に突っ走ってきた貴方を、僕は何度も止めてきました。全て自分の力だと勘違いしておられるようですが、また何か起こしてからでは遅いです。万が一貴方が泣きついてきたら、受け止めるつもりですので、まだ連絡は待ってます。

……僕の困った友人の話は、以上でした。あまり表に書く話でもないかもしれませんが、どうしても誰かに知ってもらいたかったので。

「ツクラーの毒舌な妹bot」作りました

どうも皆さん、お久しぶりです。生存報告はツイッターにしていたため、全くブログのネタがなく、長いことご無沙汰しておりました。

今日は、ツイッターbotを作った話です。

その名も「ツクラーの毒舌な妹bot

僕が作ったのは、「ツクラーの毒舌な妹bot」です。

http://twitter.com/tkooler_bot

ツクールのよくある話や薀蓄を、やや毒舌交じりに話してくれます。

ツクールのことに興味がある方で、ツイートを読んで気に入った方はフォローしてみてください。恐らくフォロー返してくれます(自動リフォロー)。

「毒舌な妹」とは

「毒舌な妹」とは、最近ツイッター上で増殖中のbotです。興味がある方は、ツイッターの検索で「毒舌な妹」で検索すると、いろいろ出てきます。

特定のジャンルで創作活動などを行う人に向けて、時にはアドバイスを、時には毒舌を喋ってくれます。

元祖は「お絵かきを勉強している人の毒舌な妹ボット」らしいですが、詳しいことは分かりません。僕は絵を描かないので、この妹さんがどのくらい毒舌なのかは分からないのですが、「プログラムを勉強している人の毒舌な妹」のツイートがリツイートされてきて、「ああ、あるある、なんてこの妹は毒舌なんだ!」と感銘を受けてフォローしてしまいました。

制作動機

プログラマの毒舌な妹」はハッシュタグで投稿も受け付けていました。面白くなっていくつかネタを提供したのですが、それが採用されて、やみつきになってしまいました。「毒舌な妹、面白い!」と思いました。何といっても、ユーザーがネタを追加していくというのが面白いと思いました。

そこでこれのツクラー版を作ったら面白いんじゃないか、と思いつきました。でも、最初は「需要あるのかなあ?」と思い、見送っていました。

しかし、最近、ちょっと不調が続いていました。何もやる気が起きない。仕事が行き詰った。そんな時、ツクラーの妹のことを思い出し、いくつかネタを出してみました。そしたら、出てくる出てくる。一日であっという間に80個のネタがまとまりました。

早速、アカウントを作り、ネタをbot作成ソフト(http://twittbot.net/)に入れました。ツクラーの毒舌botの出来上がりです。

なお、最初は差別化を図るために毒舌な弟にしようかという考えもあったのですが、タイムライン上でフォロワーの皆さんにお伺いを立てたところ、「妹がいいです」という意見が圧倒的に多く、それで普通に妹でいくことになりました。

反響

反響は予想をはるかに上回るものでした。途端にフォローされ、一日で60フォロワーまで行きました。僕のツイッター上で「bot作りました」と書くと、ツクラーさんが次々にフォローしてくれたのです。

他の毒舌な妹bot同様、ハッシュタグでの投稿も受け付けていたのですが、最初の晩はすごかったです。追加しても追加しても、次々にネタが投稿されていくのです。一気にネタの数が2倍になってしまいました。

あまりに投稿がたまりすぎたので、当初は2時間に1ツイートだったのですが、流速を上げ、最速の30分1ツイートにしました。

bot作成から一週間が経ちましたが、僕もその後もネタを追加し、投稿も加えて現在330ほどネタがあります。まだツイートされていないネタが多数あります。

アイコンについて

さて。妹botはアイコンも必要です。ツクールのRTPの顔グラフィックなどがそれらしいのですが、実はRTP素材は、ゲーム制作および紹介以外に使うのはグレーなので、オリジナルのアイコンを準備しようと思いました。

そこで、僕の好きな絵師さんにお願いしました。あまぎさん(id:amagi-k)です。よく拙作の絵も描いてくれていたのですが、独特の画風が気に入っていました。この方に「ちょっとボーイッシュで生意気な妹を描いてくれないか」とお願いしたところ快諾していただけました。

それで完成したのが、このアイコンです。せっかくなので拡大バージョンをご紹介。

ボーイッシュなため、「弟に見える」とも言われましたが、概ね好評のようです。このユニセックスな感じが、とても気に入っています。

その後の反響

ネタがリツイートされて、どんどんフォロワーさんが増えていきました。

「面白い」「参考になる」という人もいれば、一方で「ちょっと口調が妹に見えない」なんて人もいました。まあ、中の人が僕ですからね、そこはご勘弁を。

現在も、僕は思いつき次第ネタを追加していますが、それ以上にハッシュタグによる皆さんの投稿が熱い。botに更新しながら、思わずふきだすネタばかりです。

作ってみた感想

bot作成から今日で一週間になりますが、フォロワーさんも順調に増えています。中には明らかにツクール興味ないだろ、フォロワー稼ぎ……?と思われるアカウントも増えてきたのですが、来る物は拒まず。彼らにもツクールの魅力を伝えられたら、と思います。

皆さんも、思いついたら気軽にハッシュタグでツイートしてくださいね。既に同じようなネタがある場合以外は大抵採用されます。僕が詳しいツクール(作品を完成させたことある)のは、RPGツクール95、2000、XP、VX、VX Aceと、アクションゲームツクールだけなので、それ以外のツクールのネタも歓迎です。

なお、ポカもやらかしました。僕にとってはツイッター複数アカウントを運営するのはこれが初めてです。botの方でログインして、間違えて僕のアカウントのつもりでリツイートしてしまったことがあります。ツイート関係はTweenというソフトを使っているのですが、かなりの過去ログをリツイートする場合など、webをまだまだ使うため、こういうことをやってしまいます。これも、うまい方法を見つけて行きたいですね。

ムンホイXPの地味な技術:その3.フィールドマップのあれこれ

普通のRPGだと、ウィンドウのレイアウトとか戦闘のシステムやレイアウトなどに凝るものらしいですが、本作品ではそういうのは極力据え置きにして、マップ画面の仕様とかそういうものばかりいじっていました。

このため、町を移動するのが結構意味も無く楽しかったりするでしょう。それは、システム的に地味なことをたくさんしているからかもしれません。

プライオリティの導入

RGSSゲームライブラリ、Tilemapクラスにはプライオリティという概念が導入されています(RPGツクールXPのみ。VX以降は廃止)。タイルごとにプライオリティを、なし(0)から5までの間で設定することで、そのタイルの位置する「高さ」を設定しているのです。

ムンホイXPのシステムでは、この考え方を Game_Character オブジェクト(イベントやプレイヤーなど)にも導入し、タイルとの親和性を高めました。

Game_Character#priority の仕様

具体的には、Game_Character に @priority というインスタンス変数を追加しています。priority の取ることの出来る値は、タイルと同様0〜5で、これにより、同じプライオリティにあるタイルと同じ高さに表示されることになります。

これによって、様々な仕様を変更しています。例えば、Character#screen_z は、priority 導入により、それを用いた計算式に拡張しています。

プライオリティを使った多層処理

通常はプレイヤーやイベントのプライオリティは1ですが、このプライオリティを操作することで、多層処理を行っています。具体的には、以下の例が挙げられるでしょう。金網の下をくぐりぬけたり、上を通ったりすることです。


ここでプレイヤーが金網の下を通っている時のプライオリティは1、金網の上に乗っている時のプライオリティは3です。プライオリティ1の時を「下層」、プライオリティ3の時を「上層」と呼んでいます。

そして、タイルのプライオリティは次の方針に基づいて決められています。

  • 0=プレイヤーの下にあるもの(床や地面など)
  • 1=プレイヤーと同じ高さにあるもの(民家の壁など)
  • 2=プレイヤーの上にあるが上層になると下になるもの(屋根など)
  • 3=プレイヤーが上層に出たとき同じ高さにあるもの(屋根の上にある障害物)
  • 4以上=プレイヤーが上層に出ても上にあるもの(アドバルーンなど)
マップエディタでの設定方法

タイルにあったプライオリティをイベントに拡張するにあたって、イベント名に「@2」や「@4」などの「@+数字」が付いていた場合、その数字を、そのイベントのプライオリティとして設定することにしています。イベントを上層に置くためには、名前に「@3」という文字を入れると良いわけです。下層に置く場合は、デフォルトのプライオリティが1のため、わざわざ「@1」と書く必要はありません。

なお、下層と上層では、タイルの通行判定が違います。このため、上層の通行判定だけ、別のタイルセットのものを使っています。ムンホイXPでは、タイルセットはひとつにまとめており、ID=1番のものを使用していますが、上層の場合はID=2番の通行判定を使っています。

プレイヤーの上層と下層の切り替え

イベントから「スクリプトの呼び出し」で以下のようにすると変更できるようにしています。
下層に変更する場合:

$game_player.set_priority(1)

上層に変更する場合:

$game_player.set_priority(3)

これは、コモンイベントの1番(上層→通常)と2番(通常→上層)から呼び出されいます。イベントから、これらのコモンイベントを呼び出して切り替えます。ただし、実際にはもう少し複雑な処理を追加しているため、興味がある人は実際にコモンイベントおよび、そこから呼び出されているスクリプトを確認してみてください。

結論:多層処理だけなら、他のより簡単な実装方法がある

以上、ムンホイXPでの多層処理のメカニズムでした。実際には多層処理を行いたいだけなら、わざわざこんなことをしなくても、より簡単な方法はいくつかあります。

例えば、RPGツクール2000の方法として、同じタイルセット(2000ではチップセットという)グラフィックを使って、通行判定だけを変更したものを準備します。通常と上層を切り替えるタイミングで、イベントコマンド「チップセットの切り替え」を行えば、通行判定やプライオリティを変更させることが出来ます。XPには「タイルマップの切り替え」はイベントコマンドにはありませんが、同様のことはスクリプトで出来ると思います。

また、RPGツクール95では、オリジナルのムンホイでは、屋上など、上層の地形は「海」になっています。上層に出る時は、RPGツクール95特有の機能「主人公タイプの変更」でタイプを「通常」から「船」に変更することによって、海は通れるが普通の地形は通れない、つまり屋上のタイルは動けるが、別のところには行けない、というようにして実現しています。

以上を参考にすれば、より簡単な方法はあるのですが、ここではデータの一貫性があった方が拡張がしやすいと思い、今回のような方法を採用しました。

補足:この方式の採用を検討する人への注意

もうひとつ重要な注意があります。ムンホイではプレイヤーなど、主要なキャラクターの大きさが32までになっています。これはVXやVX Aceのキャラチップに近い仕様です。このため、これらのプライオリティの設定で問題ないのですが、より大きなキャラチップを使用している人が多いでしょう(32x64など)。この場合、この設定だけでは不十分な場合があります(キャラクタがプライオリティ2の屋根を突き破る、といったことがある)。このため、この考えを導入したい人は、タイルのプライオリティの値を吟味する必要があるでしょう。

斜め移動

はじめに:なぜ斜め移動はRGSSに標準搭載されないのか

斜め移動は、結構スクリプト素材も出回っており、単に斜めに移動するというだけなら、大して難しくはありません。そもそも、RGSSレベルでも、イベントコマンドでは斜め移動が標準搭載されており、これなら、プレイヤーの移動にも斜め移動が採用されてもおかしくないように思えます。(更に言えば、イベントコマンドでの斜め移動はRPGツクール2000の時代からありましたよね)

しかし、実際はRPGツクールVX Aceで、多用な機能が標準搭載されたRGSS3でさえ、斜め移動を採用しませんでした。これはなぜでしょうか。

僕に考えられる大きな理由の一つは、「ユーザー側が考慮しないと、斜め移動の見た目が不自然に見える場面が多くなるから」だと考えられます。

斜め移動が不自然に見える3つの例

まず最初に。斜め移動の通行判定は、RGSSではどのように組まれているのか、見てみましょう。左下に移動する場合(Game_Character#move_down_left)を引用します:


# 下→左、左→下 のどちらかのコースが通行可能な場合
if (passable?(@x, @y, 2) and passable?(@x, @y + 1, 4)) or
(passable?(@x, @y, 4) and passable?(@x - 1, @y, 2))

コメントにあるように、左下に行く場合「下→左、左→下 のどちらかのコースが通行可能な場合」は通行可能で、通行の処理を行うのです。

確かにこれなら、通行できないはずの地形に迷い込むようなことはありません。しかし、見た目としては、非常に不自然になることがあります。僕がテストプレイしていて見つけた(そして対処した)大きな問題点を3つにまとめ、それぞれ次の名前で呼ぶことにしました。

  • 梯子
  • 吹き抜け
  • プレート(壁)

これらを、ひとつずつ説明して行きます。

梯子

次のような移動を考えます。すなわち、階段や梯子のような地形に、斜め移動で入る(あるいは出る)場合です。

この時、「壁の中を斜めに横切って、階段(梯子)の途中の部分に飛び移っている」というように見えてしまいます。

吹き抜け

次のような移動を考えます。

この時、「何もない吹き抜けを斜めに横切って、空中移動している」ように見えてしまいます。

プレート(壁)

次のような移動を考えます。移動先は「下への移動のみが不可」という、平らな壁(プレート)のような地形です。プレートに斜めに入る(または出る)場合です。


この時、「身体半分を壁にめり込ませながら移動している」ように見えてしまいます。

斜め移動禁止地形の導入

そこで、ムンホイXPでは、タイルに「斜め移動禁止」属性を追加しました。上記の3つのうちどれであるかは、自動判定されます。すなわち……

  • 「斜め移動禁止」で、通行不可であれば、吹き抜けタイル
  • 「斜め移動禁止」で、通行可能で、プライオリティがプレイヤーより低ければ梯子タイル
  • 「斜め移動禁止」で、通行可能で、プライオリティがプレイヤーより高ければプレートタイル

ということです。

なお、この「斜め移動禁止」属性は、地形タグで実装するのが一般的でしょうが、本作では茂み(0x40)を使用していないため、茂み属性を「斜め移動禁止」として使用しています。

スクリプトでの斜め移動禁止の実装

Game_Map#ladder? で梯子判定を、 Game_Map#wellhole? で吹き抜け判定をしています。プレート判定は、梯子判定と処理が共通する部分が多いため、Game_Map#ladder? の引数を変えることで、まとめて判定します。

これらのメソッドを、斜め移動の際に呼び出し、斜め移動禁止でないか判定します。Game_Character#move_lower_left に追加された処理を引用します:


# ★斜め移動禁止地形の場合
if $game_map.ladder?(@x, @y, @priority, true) or
$game_map.ladder?(@x - 1, @y + 1, @priority, false) or
$game_map.wellhole?(@x, @y + 1, @priority) or
$game_map.wellhole?(@x - 1, @y, @priority) then
return false unless @through
end

こうすることによって、斜め移動禁止の地形で、斜め移動を行わせないことが可能です。

斜め移動が行えない場合通常移動を行うことが大事

さて。斜め移動を制限したわけですが、ここで移動の快適さのために重要なことがあります。それは、例えば「右上に動こうとして動けなかった場合、右か上に動ける場合、その方向に移動を行う」ということです。

すなわち、斜め移動が行えなくても、通常移動が可能なら、それをするということです。この考え方は、斜め移動禁止を導入しなくても重要ですが、斜め移動禁止を入れて制限を多くするなら、なおさら必要です。

なお、右上に動こうとして動けなかった場合で、右と上の両方に動ける場合はどちらに動けばいいのでしょう。どちらでもいいのでしょうが、僕は経験則から、上下の動きの方を優先しています。このあたりは、議論の余地があると思いますので皆さんも調整しながら考察するとよろしいでしょう。

これらの実装は、Game_Player#update の中に書かれていますので、興味のある方は解析してみてください。

結論:斜め移動で一番厄介なのは見た目の問題

斜め移動は常に見た目との戦いなのです。つまり、斜め移動を無くすか制限を強くすれば解決するのでしょうが、それではプレー時の爽快感が失われてしまいます。よって若干面倒でも、こだわっていきたい、そんな僕のこだわりをどうか皆さんも理解して欲しいのです。

なお、今回、斜め移動の見た目問題については、一番端的な部分を押さえたつもりですが、まだまだ、他にもあるように思えます。こういうのは、理屈ではなく見た目の問題なので、探すだけ見つかるでしょう。ぜひ、皆さんも、斜め移動を導入するときは、このような見た目の問題を意識してみてください。

移動速度(ダッシュの速度)

もう一つ小さなことですが、指摘しておきたいのは、移動速度の問題です。

RPGツクールXPの移動速度は、1〜6まで指定でき、数が大きいほど速いのです。プレイヤーのデフォルトの移動速度は4なのですが、これは少し遅く感じます。一方で5にすると、少し速く感じてしまいます。何とか、この中間が取れないものでしょうか。

本作では、ここをいじって、通常の移動速度を4と5の中間、低速ダッシュを5と6の中間、高速ダッシュを6の移動速度にしています。

実装

プレイヤーやイベントの移動速度については、Game_Character#update_move で、以下のように定義されています。


# 移動速度からマップ座標系での移動距離に変換
distance = 2 ** @move_speed

ここで @move_speed が1〜6の値を取るのです。1タイルは32ピクセルですが、これが論理座標で128の大きさになります。よって、例えば @move_speed が 6 なら、distance は2の6乗、すなわち64になり、2フレームで1タイルを移動する計算になります。@move_speed が5なら4フレーム、4なら8フレームです。

そこで、中間の速度を取るためには、3フレームや、6フレームで1タイル移動することを考えてやればいいのです。そこで、先程のコードの後に、以下を追加しています。


# ★若干移動速度アップ
if @move_speed == 4
distance = 22
elsif @move_speed == 5
distance = 43
end

これで、@move_speed が4の時、6フレームで、@move_speed が5の時、3フレームで1マス進むようになります。実際に計算すると進む座標の数は128より大きくなるように見えますが、多くなった部分は後の処理で丸められるので、問題ありません。

フレームレートをいじるよりも簡単な解決方法

ここで紹介した方法は、驚くほどシンプルに移動速度を変える方法です。

ツクールXPが出た当時は、内部フレームレートを通常の40fps から 60fps に変更することによって、移動速度を1.5倍にするという人が結構いました。CPUの性能が上がった今なら、それでも問題ないかもしれませんが、フレームレートを増やすということは、負荷を増やすということです。カクカクと処理落ちする人も多くなったのではないでしょうか。

移動速度のために処理落ちを増やすなんてナンセンスです。それよりは、こういうシンプルな方法がありますよ、ということです。

なお、この手法は、RGSS2(RPGツクールVX)以降でも比較的簡単に実装できます。

結論

以上、マップ関連のあれこれを見てきました。自分で読み返していて、つくづく地味なことやっているなあ、と痛感したのでありました。でも、僕は確信しています。本当に重要な技術は、レイアウトとか演出とかを派手にすることではなく、こういう縁の下の力持ち的な地味なものなんだ、ということを。

今回の紹介が皆さんの参考になれば幸いです。

ムンホイXPの地味な技術:その2.ビューポート分割

導入動機:天候を導入しようと思って出てきた障壁

ムンホイのXP版にあって、オリジナルになかった要素のひとつに、天候があります。雨が降ったり、雪が降ったりする演出です。これを導入した最初の動機は単純で、せっかくRPGツクールXPに、そのような機能が準備されているのだし、導入すればささやかな演出の華になるだろう、と思ったことでした。

しかし、そこにひとつの障壁がありました。それは本作のマップの構成です。

本作のマップ構成:屋内でも屋外部分が描かれている

ムンホイの屋内マップ構成の一例を次に挙げます。

ご覧になっての通り、周囲に外の景色が描かれています。これは、RPGツクール95時代には比較的一般的なやり方でした。一方、RPGツクール2000以降は屋内では周囲が黒い壁に囲まれており、屋内マップでは外の景色は描かれないのが一般的になりました。

これには理由があります。ツクール2000で初めて天候が導入されたのですが、もしこれを導入する場合、屋内に外の景色が描かれていると、その外の部分にも雨を降らせないと不自然になるからです。ツクール2000には(それ以降のツクールでも)「屋内部分では雨が降らず、外の部分にだけ雨を降らせる」といった機能はないため、屋内では単に雨をやませる、その際のマップで外の景色は描かない、というやり方が定着したのでしょう。

要求:「屋内から見える外の部分だけ雨が降っている」を実現できないか

さて。オリジナルのムンホイは天候のシステムが無かったRPGツクール95作品です。このため、天候のことを意識しないマップ構成になっています。このため、天候などは入れずに行くべきか? と思いました。

あるいは、マップの方を書き換えて外の部分を塗りつぶすという手もあるでしょう。しかしそれはしたくありませんでした。あくまでオリジナルの雰囲気を変えたくなかったし、そこに手を加えるくらいなら、天候など入れない方がいい、と。

そのように考えたのですが、最終的には「屋内から見える外の部分だけ、天候のアニメーションを表示する」という処理が実現できました。これは試行錯誤して出来たというよりは、あるとき急にひらめいて、その考えに基づいて入れてみたらすんなりうまく行った、という性質のものです。そのメカニズムを紹介していきたいと思います。

実例

カニズムの説明の前に、先にこの実装によって、具体的にどういう風になったかを、スクリーンショットを使って紹介します。百聞は一見にしかず。

この「屋内マップで描かれている屋外の部分にだけ画面処理を加える」というシステムの実装により、天気のほかに、夕焼けの描画もオリジナル版から変更することが出来ました。オリジナルの95版では、屋内でも全体が夕焼けのパレットになっていましたが、リメイク版では、屋外部分のみが夕焼けになっています。次のような感じです:

パレットが変わっているのが、屋外部分というわけです。雨のアニメーションなども、この部分にだけ描画されるのです。

具体的にどうしたのか、RGSSでの実装やマップエディタでの設定方法を紹介します。

実装:屋内と屋外でビューポートを分ける

屋外用のビューポート(名づけてビューポート0)を作成

基本的な考え方は、屋外のあるマップでは、屋内用と屋外用に、スプライトを表示するビューポートをそれぞれ用意する、というものです。

既存のRGSSでは、ビューポートは、viewport1〜viewport3 が用意されていて、viewport3 が一番手前にあり、viewport1 が奥です。そして、通常のマップ(Tilemap)は、viewport1 に表示されています。(奥にある=z座標が小さい、ということです)

今回は、そこに拡張を行います。屋内マップで屋外も表示する必要がある場合、屋内部分と屋外部分を分けて表示するために、屋内部分を既存のviewport1に、屋外部分をそれより奥にある新設したビューポートに表示することにします。この新設したビューポートは、viewport1 より奥にあるため、viewport0 と呼んでいます。

マップの屋内用と屋外用の Tilemap オブジェクトを作成し、それぞれのビューポートに置きます(どうやって屋内と屋外を分けるかは後述)。そして、天候の処理、雨や夕焼けなど、外だけに処理が必要になった時は、屋外用(ビューポート0)に対してだけその処理を行うことによって、「外側だけ、天候やパレットが変更される」という処理が実現できます。

タイルマップの要素を屋内と屋外に分ける

このように「屋内と屋外の部分でビューポートを分けて表示する」ことにしました。では、マップの要素で、どこが屋内でどこが屋外かを設定するにはどうすればいいでしょう。

マップエディタを見ると、以下のように設定されています:

マップの外側を示す部分に×、窓の部分に\という記号が置いてあるのが見えると思います。結論から言うと、これらの記号は3つあるレイヤのうちの最も手前(レイヤ2)に置かれています。×の記号は、残りの2つは外側に、\の記号はレイヤ1を内側に、一番奥(レイヤ0)を外側に置く、という意味です。(特に記号が無ければ、全て内側です)

内側と外側に分かれたタイルマップを作成する処理は、Game_AnotherMapData クラスによって行われています。Game_AnotherMapData#setup メソッドを見てもらうと分かりやすいと思います。マップから読み込んだ マップデータオブジェクト(Table)をもとに、inner および outer と名づけられたマップデータ(Table)を作成しています。これらが、内部用、外部用の Tilemap で使用されるのです。

なお、このように「屋内と屋外で表示を分ける処理を行う必要があるマップかどうか」の設定は、初期化イベントというもので設定しています。原則、マップの座標(0,0)に置かれているイベントで、イベントの名前に「!init」が含まれているものが初期化イベントと見なされます。初期化イベントのグラフィックにタイルを設定すると、そのタイルIDが、特別な処理をするものになります。ここで、×マークの描かれたタイルが設定されています。(\は、×マークの次のIDのタイルです)

このようにして、屋内用と屋外用の2枚のTilemapオブジェクトを作成し、それぞれ別のビューポートに所属させることによって、屋内と屋外を表現しているのです。

おさらい:エディタで中を見ながら確認

さて、ここまで読んで理解出来ているでしょうか。RPGツクールXPで実際にムンホイXPを読み込みスクリプトエディタを起動して、該当箇所と付き合わせながら読んでいくと理解しやすいと思います。

ここまでの理解を元に、Spriteset_Map クラスを読んでみましょう。簡略化のため、タイルマップ、パノラマ、天候の処理は、Spriteset_MapElements クラスを作成し、そちらに処理を移しているため、それらを合わせて読んでいってください。

Spriteset_MapElements#initialize メソッドを見てください。屋内と屋外に分けたデータ(AnotherMapData クラスのオブジェクト $game_map.am_data)を元に、ビューポート0とビューポート1に分けて作成している様子が分かりやすいと思います。

屋外部分にあるイベントの設定

タイルセットのビューポートを分けることが出来ました。次は、イベントの設定です。

イベントの中には、屋外部分に配置されている物もあります。例えば、上記のスクリーンショットの中では、外にカプセル(宝箱)があるのが見えると思います。ゲームをプレイした方ならお分かりでしょうが、建物の北の駐車場から回り込めば取ることが出来るのですよね!

このように、外側部分にあるイベントはビューポート0に置く必要があります。そのようなイベントは、名前に「!v0」という文字を含ませることによって設定するようにしています。具体的には、Game_Event#viewport0? というメソッドで判定しています。

そして、このメソッドでビューポート0用と判定されたイベントのスプライトはSpriteset_MapElements#add_sprite_to_viewport0 メソッドでビューポート0に配置されています。

このように、ビューポート0に配置するイベントは手動で設定する必要があるため、若干面倒ですが、これは仕方が無いですね。自動判別する仕組みが作れたら便利だったのですが。

プレイヤーが外側の部分に出るときの処理

次に考慮するべきなのは、プレイヤーが外側、つまりビューポート0の部分に出る場合です。

この場合、ビューポート0に配置するイベントと同様、プレイヤーをビューポート0に配置しなければ、パレットの変更などに対処できません。

このため、外に出るマップの場合、通常(ビューポート1)用と、外に出た時(ビューポート0)用の2種類のスプライトを作成し、現在位置のビューポートに合致する方だけを表示するようにしています。つまり、屋内にいる時は、ビューポート1用のスプライトだけを表示し、ビューポート0用は消去、逆に屋外に出たらビューポート0用を表示し、ビューポート1用は消去するということです。

この処理を実装したのが、Sprite_Character2 です。通常の Sprite_Character にビューポートの監視を行う処理を追加し、異なるときは表示を行わないようにしています。

このビューポートの監視に使う変数が、Game_Player に追加された @viewport_id というインスタンス変数です。プレイヤーが内側にいる時(あるいはビューポートを分けない場合)は 1 を、外側の時は 0 を取るようにしています。

ビューポートの切り替え(内側と外側を切り替える)は、イベントで行っています。コモンイベントの12番と13番に「ビューポート0」と「ビューポート1」というのがありますが、これをイベントから呼び出すことで、切り替えているのです。

結論:限りなく地味な技術。僕自身、他の作品で採用するかどうか不明

解説は以上ですが、いかがだったでしょうか。この「外だけ雨が降っている」という処理、スーファミなどのRPGでは、普通に見られた処理なので、本作で見かけても目新しいとは思えなかったと思いますが、ツクールの他の作品で見かけたことがないという人も少なくないのではないでしょうか。

無理もありません。外と内を分ける為には、マップ設定から若干ややこしい処理を行う必要があるので、なかなかやる人はいないでしょう。現にこの僕も、リメイクということで元のマップを変更したくないために行った処理です。1から新作を作る場合は、こんなややこしい処理を入れる代わりに、外側を黒く塗りつぶしていたと思うのです。

このため、この技術は本作のみのものになるかもしれません。しかしこの「Tilemapを2枚作り別々のビューポートを作る」という発想は、何かあなたの制作のヒントになるかもしれないと思い、紹介させていただきました。RGSSでこのあたりを大胆にいじっている人は少ないのですが、このあたりの改変こそが、RGSSの醍醐味だと僕は思っているのです。

ムンホイXPの地味な技術:その1.軽量化

ムンホイXPは、実は結構軽いです。どのくらい軽いかというと、RPGツクールXPのデフォルトの推奨環境を下回っていても、それなりに動くくらいです。他のツクール作品をプレイしてかくかくした経験がある人でも、本作でかくかく処理落ちがしなかった、という人は結構いるんじゃないでしょうか。これは、独自の軽量化を施しているからなのです。

軽量化を施した経緯

そもそも、じゃあなんで本作は軽量化を施したのでしょうか。それは、開発当初の共同制作者の開発環境にあります。その話をしましょう。

当時の僕の共同制作者は、過半数RPGツクールXPの必須動作環境(PentiumIII 800MHz)以下の環境でした。僕がムンホイのフィールドのテストマップを半分ほど作り、テストしてもらっていると、こういう人がいました。「バージョンアップごとに、どんどん重たくなっていく。うちのPCでは重すぎて動かせない。自分はテストプレイに加われないかもしれない」と。僕のPCは当時は結構ハイスペックな物だったので重たいなんて思いもよらなかったのです。

それを聞いて僕は思いました。「RGSSには軽量化の余地はあるのでは?」と。当時、僕はRGSSを解析していたのですが、どうも最適化されていない部分を発見していました。「僕ならこうするのに」と。でも、こうなっているのだから、これが最善なのだろう、と思っていました。しかし、それを最適化したら、もしかしたら、軽くなるかもしれない、と思いなおしたのです。僕はさっそく、自分が感じた部分の軽量化を施してみました。

そうしたら「重くて動かせない」と言っていた彼はこう言ってきました。「凄い!ほとんどかくかくしなくなりました!」と。そうです、軽量化は功を奏したのです。

2005〜2006年当時、ネット界隈でも「RPGツクールXPは重い」という意見が支配的でした。フリゲをやっている人でそれなら、拙作をプレイする、フリゲを普段やらない層なら、なおさらPCのスペックは低いはず。ならば、まず、可能な部分は軽量化しておかないと「重たい」と言われて飛びついてはもらえないと思ったのです。

こういうノウハウは、一度確立しておけば、他の作品でも流用できるということもあり、僕は軽量化ノウハウの確立を最優先しました。なぜなら、当時のRGSS界隈を見ても、「ウィンドウの改良」とか「自作戦闘」だとか、演出の改良ばかりで、軽量化はほとんど扱われておらず、皆、「俺の作品をやりたいならてめえでいいPC準備しろ」と言わんばかりでした。自分でやるしかないと悟ったのでした。

皮肉なことに、ムンホイXPの公開は2011年となり、2005年当時のPentiumIIIから、CPUのメインは2〜3世代進歩し、僕の施した軽量化はなくても、それなりに動く環境になってしまいました。とはいえ、未だに低スペックのPCでプレイする人は少なくなく、そういう人が軽量化を評価してくれたことは嬉しかったです。

この項では、ムンホイXPが施した軽量化のうち、特に効果があった、しかもRGSS3でも導入されていないようなものを中心に紹介していこうと思います。

まず最初に:RGSSに頼らない軽量化

最初に僕が取り掛かるべきだと思うのは、スクリプトではない部分の軽量化です。具体的にはマップやイベントの置き方の無駄をなくすことで軽量化するということです。実はこれが一番効果的だったりしますので、これをまず最初に紹介します。

必要ない場面ではパノラマを切る

RPGツクールXPでは、パノラマはタイルセット単位でのということもあり、必要のないマップでもパノラマが表示されていることもあるでしょう。実はパノラマが表示されない場合でも、システムはまずパノラマを描画してから、タイルセットを描画しています。このため、パノラマ描画の分だけ、処理が重たくなっているのです。このため、必要のないマップでは、タイルセット指定のパノラマを切ってしまいましょう。

具体的には、マップ読み込み時に自動実行イベントを実行し、そこでパノラマの設定を切るのです。これだけでも相当違ってくるはずです。

見えない部分のタイルは(通行判定以外は)置かない

RPGツクールXPでは、3層のタイルマップ描画が可能です。このため、タイルマップを重ねてマップを描く人が多かったと思います。しかしここに罠があります。重なって画面に見えない部分であっても、律儀に描画しているわけで、その分重たくなります。

もし、画面に見えず、移動判定にも使用しないタイルがあったら、それらは極力置かない方針にしてみましょう。ちりも積もれば山となるで、かなり軽くなります。

ムンホイXPで参考になるのは、フィールド画面(もとまちタウン、もとまちシティ)です。かなり画面が広く、イベントも多くて重たくなっていましたが、余計なタイルを描画しない構造に変えることで、かなり軽くなりました。

必要のないイベントは「一時消去」する

これはどのツクールでも共通ですが、マップを出るまでもう起きるはずのないイベントは、「一時消去」するといいです。多くの人はページを切り替えて透明のページを表示させているでしょうが、「一時消去」すると、数々の処理をパスするため、それだけでかなり軽くなります。さらに後述する今回僕が導入した手法を追加すると、さらに軽くなります。

軽量化手法その1:イベントの位置情報テーブルを作る

まず、RGSS1のプリセットスクリプトの通行判定、Game_Map#passable および Game_Character#passable を見てください。以下のような部分が見られます。


# 全イベントのループ
for event in $game_map.events.values
# イベントの座標が移動先と一致した場合
if event.x == new_x and event.y == new_y

すなわち、マップ上の全てのイベントと位置を照合し、一致するイベントに対して処理を行っているのです。

しかしこのループ、イベントが増えるごとに、処理が増えていくのは明白です。イベントnに対して、O(n)(nのオーダー)で処理が増えて行きます。

しかも、毎フレーム、(1(プレイヤー)+移動するイベントの数)だけ、この処理が行われているのです!マップにN分の1の割合で自律移動するイベントがあった場合、全体的な処理は、O(n^2)(nの2乗のオーダー)で増えることになってしまいます。

しかし、もし、「それぞれの座標にあるイベントのテーブル」が存在するとしたらどうでしょう?例えば、{ イベントの座標 => そこにあるイベントIDの配列 }といったハッシュがあれば、O(n)のループをする必要がなくなり、O(1)で取り出せることになります。全体的な処理も、O(n^2)から、O(n)になりますよね。

以上の処理を実現するために、ムンホイXPでは、EventPositionTable クラスを実装しました。具体的には Hash のサブクラスであり、前述の発想にもとづいています。これのインスタンス $game_map.event_at を導入することで、イベントを見るループを全て、このハッシュを参照する処理に置き換えました。これらのキーワードでスクリプトを検索すると、メカニズムが分かると思います。

この軽量化が効果を施す例

この軽量化は、フィールドマップのように、イベント数が多いマップで如実に効果を発揮します。

あなたの制作でも、イベントが100個を超えたあたりから、如実に移動時にカクカクし始めたのではないでしょうか。僕もそうでした。なにしろイベント数nに対し、n^2のオーダーで処理が増えていたのだから当然です。この軽量化を施すことで、nのオーダーになるため、非常に軽くなります。

結果、ムンホイXPでは、最大で180個のイベントがあるマップがあります(ぜのんの町のフィールドがそれです)が、それほど重たくなっていません。その軽さに一番貢献しているのは、この処理なのです。

Hashで実装する理由

さて。この実装に、僕は Hash を使いましたが、なぜなのでしょう。似たようなメカニズムを見たことがありますが、その人は RGSSのゲームライブラリの Table クラスを使っていました。一見この方が効率がよさそうです。しかし、Tableクラスが値として取れるのは、符号付16ビットの数値(-32768〜32767)だけです。これだと問題が起きることがあります。それは、複数のイベントが、同じ座標に位置した場合です。

イベントには「すり抜け可能」オプションがあります。これがONの場合は、複数のイベントが同じ座標に位置することが可能です。TableクラスでひとつのイベントのIDだけを記録する方法だと、この場合に対処できません。それで Hashを使い、値はIDの配列を取るようにしたわけです。

RGSS2以降で実装する場合

ちなみにムンホイXPはツクールXP、すなわちRGSS1で実装していましたが、同じ考えはRGSS2以降でも可能であり、さらに簡単に実装が可能でしょう。

それは、Game_Map#events_xy メソッドが導入されたことです。このメソッドは指定座標に存在するイベントの配列を取得するものであり、ここを 前述の Hash を用いたメソッドに書き換えることにより(そしてイベント移動時に Hash の情報を書き換えることにより)、よりコンパクトに実装が可能です。ひょっとしたらこの軽量化スクリプトは探せばあるかもしれませんね。

なぜRGSSはこの軽量化を見送ったか

さて、それでは大きな疑問があります。「なぜ、こんなに簡単に実現できる軽量化を、RGSSは見送ったのか」ということです。現にこの軽量化はRGSS3でも実装されていません。

これには理由はあります。ずばり、「この軽量化を行うと、拡張性が著しく低下するから」です。ここを説明しましょう。

イベントの位置座標を、別のテーブル(EventPositionTable)に保存するということはすなわち、イベントのxとyの位置情報が、Game_EventクラスとEventPositionTableという、複数の場所に保存されていることになります。すなわち、スクリプトの拡張を考える人は、これらのデータの一意性を意識しなければならなくなります。もし、Game_Eventのxやyの値を不用意に書き換えた後、EventPositionTableのことを忘れて、書き換えなかったらどうなるでしょう……たちまちデータの整合性が崩れてしまうのです!

このように、スクリプトの拡張性を吟味したRGSSでは、このような「データの一意性」を崩す軽量化を考えるよりも、若干重たくなっても、拡張の容易さ、安全さを選択したのだと言えます。逆に言うと、この軽量化を選択する人は、スクリプトでxやyを直接いじるような拡張を本当にやらないと言えるのか、そういった拡張を行う場合、EventPositionTable 側への反映を意識して行う必要があるといえます。

つまり、軽量化全体にいえることですが、1つ軽量化を施すというのは、ひとつの機能を追加する場合と同様に、拡張性を1つ損ねてしまうといえます。拡張が不可能にはならないのですが、拡張に際して、軽量化を施した部分を意識して組むなど、余計な労力を要求するようになります。だから、軽量化についても、むやみに導入するのではなく、「差し迫って必要のない軽量化は導入しない」「軽量化を施す場合は、それによって拡張性を少なからず犠牲にする」ということを肝に銘じる必要があります。本作では、リメイクということもあり、余計な拡張は一切必要ないと断定できたからこそ、大胆な軽量化が出来たのです。あなたの作品ではどうでしょう。慎重に吟味してください。

軽量化手法その2:不要なスプライトを作成しない

次に効果が大きかった軽量化は、「不要なスプライト(Sprite オブジェクト)を作成しない」ということでした。

まず最初に知っていてもらいたいのは、Sprite およびそのサブクラスのオブジェクトは、生成されたときにシステムに登録され、Graphics.update が呼ばれた時に一気にそれぞれのスプライトの更新が反映される形になっています。つまり、スプライトは、オブジェクトを生成しただけで、特に表示などを行わなくても、システムに多少の負荷をかけているのです。

つまり、必要のないスプライトを最初から生成しないことが出来たら、それだけで軽量化が出来ることになります。

RGSS3で導入された「スプライトを生成しない技術」

RGSS3では一部、この手の軽量化が採用されました。それは、天候(RGSS3:Spriteset_Weather, RGSS1:RPG::Weather)とピクチャ(Sprite_Picture)です。いずれも、RGSS2までは、必要な最大数(天候は40個、ピクチャはRGSS1では50個)を最初に作成しており、それだけの数が存在しなくても常に最大数のスプライトが存在していましたが、RGSS3では、必要なだけしか存在させず、必要になったときに生成、必要なくなったら廃棄(dispose)させることで軽量化を図っています。

なお、ムンホイXPでも同様の軽量化を行っていますが、せっかくRGSS3に導入されている技術なので、細かい説明はそちらをご覧になったほうがいいと思います。

Game_Character に対応する Sprite_Character を作らなくてもいい

さて、ムンホイXPは、もう一歩踏み込んだ「余計なスプライトを作成しない」をしています。それは、「イベントのスプライトも、必要がなければ作らない」というものです。

一般的に、イベントのオブジェクトは Game_Event というクラスが担当しており、その多くの挙動はスーパークラスの Game_Character が担っています。Game_Character に対応するスプライトとして Sprite_Character が作成され、このスプライトは対応する Game_Character を監視し、スプライトの変更を行います。つまり、Game_Character と Sprite_Character は1対1対応しているということです。

しかし、です。イベントの中には画像を表示しないものもあります。たとえば単なる場所移動のイベントなどです。これらは Sprite_Character オブジェクトは透明なスプライトを表示し続けるだけで、実質なにもしていません。ならば、そういうスプライトは作成しなくてもいいのでは?という考えに基づいて、「必要ないスプライトは作成せずに軽量化」を図りました。結果的に、7〜9割のイベントでは、スプライト作成の必要がなかった、という衝撃的な結果が出ました。具体的には、ぜのんの町のマップでは180個のイベントが置かれていますが、スプライト作成の必要性があったのは、30個前後でした。実に6分の1です。

では、「どういう条件でスプライトを非作成にするか」「どういう点に注意をすべきか」を説明して行きます。

スプライト非作成の条件

まず、スプライトを作らない条件についてです。これは吟味の余地がありますが、ムンホイXPでは、以下のいずれかの条件に合致していれば、スプライトを作らないイベントとしています。

  • 全ページ数が1で、イベントグラフィックが設定されていない
  • イベントの起動時にそのイベントが既に消去されている。つまり、「イベントの並列実行」で「イベントの一時消去」が実行された

スプライトを作成しているかどうかを判定するために、インスタンス変数「スプライト非作成フラグ( @non_sprite )」を導入しました。これに該当するイベントは、スプライトを作成しないことにしたわけです。

スプライトが必要なときは、動的生成する

しかし、ここで問題があります。上記を満たすイベントでも、スプライトが必要になる場合があります。それは下記のような場合です。

  • イベントコマンド「移動ルートの設定」で「グラフィックの変更」が行われた場合
  • イベントコマンド「アニメーションの表示」で対象キャラクターとして当該イベントが指定された場合

特に、一時消去されたイベントであっても、その位置にスプライトがないと、アニメーションが正常に表示されないということは盲点です。

このような場合、ムンホイXPでは、「スプライトを動的に生成する」という方法で対処しています。

具体的には、@dynamic_create_queue というインスタンス変数を $game_map に追加し、ここにイベントIDが追加された場合、Spriteset_Map#update にて、動的にスプライトを生成する処理を追加しています。

この、「必要なときに動的に追加する」という処理は、かなりトリッキーに見えますが、驚くほど問題は起きていません。

RGSSがこの軽量化を行わない理由

以上のように、若干ややこしいのですが、RGSSはこのような軽量化は行っていませんし、今後も行わないと思います。

なぜか。それは、「スプライトを作成しているかどうか」を常に意識しながら「動的に生成するかどうか」という処理を追加するか考えていくというのは、非常にややこしいことだからです。

天候やピクチャであれば、それをいちいち改変する人は少ないでしょうが、イベントであれば、改変する人も少なくないでしょう。そういう人たちにいちいち「このイベントのスプライトが作成されていない場合もあるから、拡張する場合は適宜それを意識してコーディングすること」というのはあまりにも面倒です。

このように、この軽量化は、イベントに様々な拡張を施したい場合、結構大変なことになります。十分理解したうえで、素材を導入する場合特に、相性を吟味する必要があります。

軽量化手法その3:Game_Character をあれこれ軽量化

Game_Character は、マップのイベントの数(+プレイヤーなどの数)だけ存在し、それらのオブジェクトが毎フレームごとに update を呼び出します。このため、このあたりを軽量化できたら、イベントの多いマップでは、かなりの軽量化に繋がります。

RGSS3でも導入された、「画面外か」という考え方

まず、軽量化の方法として考えられるのは、「画面外なら、update の処理をあれこれ省略する」というものです。

この考え方は RGSS3 では一部取り入れられており、Game_Event#near_the_screen? メソッドの導入により、「画面に近い」以外のイベントは、一部の処理を省略しています。

ムンホイXPでも、同じような軽量化を導入しています。ムンホイXPでは、Game_Character#on_screen? メソッドを導入し、スプライトのサイズなどから、より厳密に(その分計算量は増えますが)画面内かどうかを判定し、同じ計算を何度も計算せずに済むように一度計算したらインスタンス変数 @on_screen に入れています。@on_screen が false の場合、一部の処理をスキップしているのです。

「置物」すなわちアニメーションも移動もしないイベント

次に考えられるのは、「置物のようにアニメーションも移動もしないイベントは、それらの処理を省略する」というものです。

プリセットスクリプトのGame_Character#update は、たとえアニメーションも移動も行わないイベントであっても、jumping? や moving? の判定を行ったうえ、Game_Character#update_stop を呼び出すなど、かなり手間のかかる処理を行っています。これは、全てのイベントに共通の処理を行うことで処理を簡略化しているのですが、置物系のイベントでは、明らかに冗長です。無駄な処理をスキップできたら、かなり軽くなるのではないでしょうか。

「置物」の条件

それで「置物」の条件を考えましょう。Game_Event#static? というメソッドを作り、そこで定義しています。具体的には、以下の通りです。


def static?
# ※移動ルート『固定』かつ、(キャラクタでないか停止時アニメなし)
return (@move_type == 0) && (@character_name == "" || !@step_anime)
end

これが成り立つことが、置物であることの必要十分条件なのです。

実際にはこの値をインスタンス変数 @static に保存して、それを呼び出しています。これが成り立つイベントは、update の大半をスキップできます。

画面上には、予想以上に「置物」となるイベントが多いです。宝箱のようにアニメーションしないものはあなたの作るマップにも多いのではないでしょうか。

置物イベントに関する注意

さて、置物イベントですが、場合によっては置物ではなくなる場合があります。それは以下の場合です。

  • イベントページが切り替わった場合
  • イベントコマンド「移動ルートの設定」が行われた場合

前者の場合は、改めて static? を呼び出し、@static を更新します。

後者の場合は、イベントコマンドによる移動中は @static を false にし、移動が終了した時点で改めて static? を呼び出し @static を更新します。

導入時には注意が必要:拡張性の問題

以上、Game_Character まわりの軽量化を見てきましたが、いかがだったでしょう。

他の軽量化と同様、この軽量化も、プログラムが複雑化するリスクをはらんでいることがお分かりだと思います。くれぐれも、アクションRPGのように、イベント関連にスクリプトで手を加えるシステムを作る人は、それらの実装がおおむね完了してから、それらの兼ね合いを考えて入れていくことをおすすめします。

軽量化手法その4:Sprite_Character をあれこれ軽量化

Game_Character と同様、マップに多数配置されており、毎フレームごとにそれに比例する update が大量に呼び出されるのが、スプライトです。軽量化その2によって、余分なスプライトを作らない場合でも、まだ一定数のスプライトを作る必要があるわけで、だめを押しておきたいところです。そこで、Sprite_Character#update について、以下のような軽量化が考えられます。

Sprite#updateを呼び出さない禁じ手

Sprite_Character#update は、super によって、スーパークラスの update メソッドを呼び出しています。しかし、ヘルプのSprite#updateには、以下のように書かれています。

フラッシュの必要がない場合は呼び出さなくても構いません。

同様に、Sprite_Character の直接のスーパークラスである RPG::Sprite でも同様です。ダメージの表示などの処理を行っているのですが、これらが必要にあるのは原則戦闘中のみであり、アクションRPGのように「移動中にダメージを表示したりフラッシュさせる」という処理を追加しない限りは、super(RPG::Sprite#update)を呼び出す必要がないのです。

このため、「アニメーションの表示中以外は、superを呼び出さない」という処理を行っています。RPG::Sprite#update はかなり様々な判定を行っているため、この些細な処理がかなりの軽量化になります。

もちろん、このような処理が許されるのは、イベントの拡張を行わない場合のみであり、アクションRPGにありがちな「移動中にダメージを表示」などを追加したい場合、この軽量化は行ってはいけません。

なお、RGSS1の RPG::Sprite は、RGSS2以降は Sprite_Base になっていますが、処理が簡略化されており、この軽量化は意識する必要がありません。

グラフィック転送を毎フレームは行わない

もうひとつ。Sprite_Character#update を見ていると、以下のような行を見つけます。


# グラフィックがキャラクターの場合
if @tile_id == 0
# 転送元の矩形を設定
sx = @character.pattern * @cw
sy = (@character.direction - 2) / 2 * @ch
self.src_rect.set(sx, sy, @cw, @ch)
end

ここで self.src_rect.set というのは、コメントの通り、転送元の矩形を設定しているわけですが、毎フレームごとに設定するのは、若干処理の負担になっているのではないでしょうか。転送元が変更された場合のみでいいんじゃないでしょうか。そこで、以下のように条件を追加し、変更します。



# ★(軽量化)パターンか向きのいずれかが変化した場合
if @tile_id == 0 and (@pattern != @character.pattern or
@direction != @character.direction) then
# 転送元の矩形を設定
self.src_rect.set(
@character.pattern * @cw,
(@character.direction - 2) / 2 * @ch,
@cw, @ch
)
# ★パターンおよび向きを更新
@pattern = @character.pattern
@direction = @character.direction
end

@pattern と @direction というのが新たに追加されたインスタンス変数です。これらは、グラフィックが変更されたときは、nil に初期化します。

「これ、本当に効果あるの?」とお思いでしょうが、これを導入して、実際にベンチマークを行うと、かなり軽くなりました。同様の処理は、RGSS2、RGSS3でも毎フレーム行われているため、軽量化を追及する人は追加してもいいと思います。

その他の軽量化手法あれこれ

他にも様々な軽量化を施しています。特にウィンドウ関係の軽量化は地味にいろいろやっていますが、特筆すべきものをひとつ。

文字をキャッシュに入れる

一度表示した文字をキャッシュに入れて、今後表示するときはキャッシュから描画(Bitmap#bltにて描画)しています。

DrawTextCacheクラスというのを作り、ここに文字のキャッシュを入れています。インスタンスは$text_cache という名前にしており、スクリプト内の Bitmap#draw_text を呼び出しているところを、DrawTextCache#draw に置き換えることにより、実装しています。

これは、当初のPCでは、文字の描画が遅く、文字を描くくらいならBitmap#bltで描画した方が早いため考えたメカニズムですが、最近のPCでは、文字の描画速度はあまり問題にならないようなので、あまり導入の意義は小さいと思います。

軽量化、結論

全体的に言えるのは、最近のPCでは、軽量化の意義は薄れてしまったこと、軽量化を1つ追加するのは、拡張性の幅を1つ狭めてしまうことになるため慎重に行う必要があることがあります。

ただ、そんな中でも、「イベントの位置テーブルを作る」という考え方だけは、今でも意味があると思います。イベントの数の2乗に比例して処理が増えるというのは、とてもCPUパワーの上昇だけでは補えるものではなく、多くのイベントを置いたマップを作る人はぜひ思い出してもらいたいものです。

これまでなら、イベントの多いマップを作るのを断念して、いくつかのマップに分ける人がいたでしょう。しかし、そうせずに済む方法もあるということです。今回のムンホイのリメイクのように、どうしても元の仕様を変えたくないような人は検討する価値があるでしょう。

今日は軽量化についてぼちぼち書いてみました。あともう少し地味な技術について、そのうち書いてみようと思います。