男性キャラクターがメビウスの輪のステージで、アイテムを裏側に投げて性質を反転させ、その性質を使ってゴールを目指すパズルゲーム。
メビウスの輪の上を移動するには、重力の方向も移動する方向も常に一定ではないので、普通の移動では難しい。よって私はウェイポイントを使ってメビウスの輪の上の移動を実装した。ここでいうウェイポイントとは位置情報と回転情報を保持したデータのことを指す。そのウェイポイントをメビウスの輪の上に配置する。このゲームでは32個のウェイポイントが配置されている。
そして以下の手順で移動する。
しかし、この方法ではウェイポイントの間を直線的に移動することしかできない。さらに地面からも離れている。そうではなくメビウスの輪に沿って移動して欲しい。
そのため、下向きのベクトルを補完済みの回転情報で回転させ、その方向にプレイヤーからレイを飛ばし、レイとメビウスの輪の交差点を求め(次項の4.2.メッシュとレイの交差点を求めるで説明)、その座標をプレイヤーの座標とすることでメビウスの輪に沿った移動にする。
つまり最終的な手順は
となる。
メッシュとレイの交差点を求めるには以下の処理手順で計算する。
それぞれ手順を詳しくみていく。
「三角形ポリゴンを含む無限平面と線分(レイ)の交差判定。」
メッシュから三角形ポリゴンを一つ取り出して、三つの頂点の内どれか一つの頂点の座標、その頂点の法線(正規化済み)、レイの始点、レイの終点の四つのデータを使う。
頂点座標からレイの始点へのベクトルvertToStartVec、頂点座標からレイの終点へのベクトルvertToEndVecの二つのベクトルを求め、それぞれ正規化しておく。
頂点の法線と正規化されたvertToStartVecの内積normAndStartDotと、頂点の法線と正規化されたvertToEndVecの内積normAndEndDotを求める。
normAndStartDotとnormAndEndDotの積が負の値だったら無限平面と交差しているので次の工程へ。0以上の値だったら交差していないので次の三角形ポリゴンを調べる。
「交差している座標の計算。」
無限平面とレイの交点とレイを使って図のような三角形を二つ作る。この二つの三角形は相似のため対応する辺の比が等しい。レイの始点と終点の座標は分かっているので、辺の比が分かれば無限平面とレイの交点の座標が求めることができる。
辺の比を求めるために下の図の場所の辺の長さを調べる。
辺の長さを調べるために、まず三角形のどれか一つの頂点座標からレイの始点へのベクトルとvertToStartVec、頂点座標からレイの終点へのベクトルvertToEndVecの二つのベクトルを求める。今回は正規化しない。そして反転した頂点の法線も求めておく。
頂点の法線とvertToStartVecの内積normAndStartDotと、反転した頂点の法線とvertToEndVecの内積normAndEndDotを求める。これで頂点の法線に射影したvertToStartVecの長さと、反転した頂点の法線に射影したvertToEndVecの長さを求めることができる。
この二つの長さは、図の位置の辺の長さと等しい。つまり、この二つの三角形の相似比はnormAndStartDot : normAndEndDotになる。
相似比が分かったので、レイの始点から終点へのベクトルstartToEndVecを求めて normAndStartDot / (normAndStartDot + normAndEndDot) を掛けるとレイの始点から交点までのベクトルstartToIntersectVecが求まる。
レイの始点とstartToIntersectVecの和が無限平面とレイの交差点の座標になる。
「2で求めた交差点の座標が三角形の中にあるかどうか判定。」
今までは三角形ポリゴンを真横から見て考えていたが、次は真上から見て考える。2で求めた交差点の座標と、三角形ポリゴンの頂点座標三つのデータを使う。
三角形の頂点それぞれから無限平面とレイの交差点へのベクトルvert1~3ToIntersectVecを求める。そして三角形を時計回りに回るように、頂点1から頂点2へのベクトルvert1ToVert2Vec、頂点2から頂点3へのベクトルvert2ToVert3Vec、頂点3から頂点0へのベクトルvert3ToVert1Vecを求める。
三角形の頂点それぞれから伸びている二つベクトルを、終点が交点の方のベクトル(vert1~3ToIntersectVec)、もう一方のベクトル、の順番で外積を求めるcross1~3。そして、それぞれ正規化しておく。
cross1とcorss2の内積dot1、cross1とcross2の内積dot2を求める。dot1とdot2が両方正の値だったら交点が三角形の中にあるため、レイと三角形ポリゴンが交差している。
「メッシュの三角形ポリゴン全てに1~3の処理を行う。その中で、複数の交差点が存在したら、レイの始点から一番近い座標を交差点とする。」
「交差点が存在しなければ、交差していない。」
それぞれ手順を詳しくみていく。
「分離軸を探す。」
3D空間のOBBの分離軸は、片方のOBBの単位方向ベクトル3本、他方のOBBの単位方向ベクトル3本、双方の単位方向ベクトルに垂直な分離軸3x3=9本の計15本が必要になる。
双方の単位方向ベクトルに垂直な分離軸は双方の単位方向ベクトルの外積で求めることができる(その後正規化する)。
「分離軸に二つのOBBを射影して射影線分を作る。」
分離軸は単位ベクトルなので、ベクトルと内積を取ると射影線分を求めることができる。だから分離軸と、OBBのそれぞれ大きさ付の方向ベクトルのとの内積の絶対値を合計すると射影線分の半分の長さが求まる。
分離軸が自身の単位方向ベクトルの時は、大きさ付の方向ベクトルの大きさがそのまま射影線分の半分の長さとなる。
「二つの射影線分が重なっていないか調べる。」
OBB同士の中心点の距離が、双方の射影線分の半分の長さの和より大きいときは、重なっていないので衝突していない。
OBB同士の中心点の距離が、双方の射影線分の半分の長さの和より小さいときは、重なっているので、この分離軸では衝突している。
「1~3の処理を全ての分離軸で調べる。重なっていない射影線分が一つでもあれば衝突していないので処理を抜ける。」
「全ての分離軸で重なっていたら衝突している。」
このtklファイルを以下の処理手順で読み込んでいく。
レベル遷移時に急に場面が切り替わるのではなく、ワイプで画像を表示してから、見えないように画像の後ろでレベル遷移を行うようにしている。ワイプの種類は以下の5種類あり、ランダムでワイプするようになっている。
リニアワイプ
リニアワイプでは、CUP側(C++)とGPU側(HLSL)の両方の処理で行う。
CPU側では、現在どのくらいワイプしているかの変数wipeSize、ワイプする方向wipeDirを用意し、GPUに渡すようにする。
wipeSizeを毎フレーム加算することでワイプを進める。
GPU側では、wipeDirと描画するピクセル座標の内積をとり、wipeDirに射影したピクセル座標の長さwipeProjLenを求める。
wipeProjLenがwipeSizeより大きかったら通常の描画をして、小さかったらワイプ用のスプライトを描画する。
円形ワイプ
円形ワイプでは、CUP側で、現在どのくらいワイプしているかの変数wipeSizeを用意し、GPUに渡すようにする。
wipeSizeを毎フレーム加算することでワイプを進める。
GPU側では、画面中央からピクセル座標への距離posFromCenterを求める。
posFromCenterがwipeSizeより大きかったら通常の描画をして、小さかったらワイプ用のスプライトを描画する。
縦縞ワイプ
縦縞ワイプでは、CUP側で、現在どのくらいワイプしているかの変数wipeSizeを用意し、GPUに渡すようにする。
wipeSizeを毎フレーム加算することでワイプを進める.
GPU側では、画面の横幅 / 分割したい数 で分割された一つ分の横幅divideLengthを求める。
ピクセル座標のX座標をdivideLengthで割った余りdividedPosを求める。これによって画面を縦に分割することができる。
dividedPosがwipeSizeより大きかったら通常の描画をして、小さかったらワイプ用のスプライトを描画する。
横縞ワイプ
先ほどの縦縞ワイプとほぼ同じ。横縞ワイプではCUP側で、現在どのくらいワイプしているかの変数wipeSizeを用意し、GPUに渡すようにする。
wipeSizeを毎フレーム加算することでワイプを進める.
GPU側では、画面の縦幅 / 分割したい数 で分割された一つ分の縦幅divideLengthを求める。
ピクセル座標のY座標をdivideLengthで割った余りdividedPosを求める。これによって画面を横に分割できる。
dividedPosがwipeSizeより大きかったら通常の描画をして、小さかったらワイプ用のスプライトを描画する。
チェッカーボードワイプ
縦縞と横縞のワイプを合わせたワイプ。チェッカーボードワイプではCUP側で、現在どのくらいワイプしているかの変数wipeSizeを用意し、GPUに渡すようにする。
wipeSizeを毎フレーム加算することでワイプを進める.
GPU側では、画面の横幅 / 分割したい数 で分割された一つ分の横幅divideLengthXと、画面の縦幅 / 分割したい数 で分割された一つ分の縦幅divideLengthYを求める。
まずは、縦分割から考えていく。ピクセル座標のY座標をdivideLengthYで割って、ピクセル座標が偶数段目にいるのか奇数段目にいるのか調べる。
次に、横分割を考える。偶数段目の時は普通の縦縞ワイプと同じようにする。奇数段目の時は、ピクセル座標のX座標をdivideLengthXの半分ずらして縦縞ワイプをさせる。
このゲームでは空の表現にキューブマップスカイボックスを使用している。キューブマップスカイボックスとは、空用の球体のモデルのピクセルカラーを、球体の法線の先にあるキューブマップのカラーにする、というものである。詳しい手順は以下の通りである。
キューブマップを用意する。キューブマップとは下の画像のように立方体の展開図のような形をした画像である。
空用の球体のモデルのシェーダーリソースビューにキューブマップを登録する。この時D3D12_SHADER_RESOURCE_VIEW_DESCのViewDimensionをD3D12_SRV_DIMENSION_TEXTURECUBEにしなくてはいけない。
GPU側では、TextureCubeの型でキューブマップを受取って、球の法線を使ってサンプリングする。球の法線の先にあるキューブマップのテクスチャのカラーがそのピクセルのカラーになる。
ポストエフェクトの一つであるブルームを、川瀬正樹氏が発表した川瀬式ブルームを使用して実装した。
普通のブルームはレンダリング後の画面から輝度抽出し、明るい部分にガウシアンブラーをかけて、もとの画面に加算合成するものだ。しかし、このやり方だとある程度明るい光は全て同じ半径のブルームになってしまう。
一方、川瀬式ブルームはダウンサンプリングしながら複数回ガウシアンブラーをかけ、それらを平均化したものを元の画面に加算合成する。これにより大きい光はより大きい半径のブルームがかかるようになる。
詳しい手順は以下の通りである。
モデルなどを全てレンダリングする。
レンダリング後の画面から輝度抽出をする。
輝度抽出したテクスチャをダウンサンプリングして、ガウシアンブラーをかけボケ画像を作る。ダウンサンプリングすると解像度が低くなるため、ガウシアンブラーの処理が軽くなる。(1280×720 -> 640×360)
3.で作ったボケ画像を、さらにダウンサンプリングしてガウシアンブラーをかける。(640x360 -> 320x180)
4.で作ったボケ画像を、さらにダウンサンプリングしてガウシアンブラーをかける。(320x180 -> 160x90)
5.で作ったボケ画像を、さらにダウンサンプリングしてガウシアンブラーをかける。(160x90 -> 80x45)
出来上がった四つのボケ画像を同じ解像度になるように拡大し、加算合成したあと平均を取り、それを描画する。
VSM、分散シャドウマップとは、深度値のグループごとの局所的な分散を利用するソフトシャドウの一種である。深度値の局所的な分散が大きい場合、そのグループ内の深度値の幅が大きいということであり、つまり、影の境界線であることがわかる。影の境界線はジャギーが発生しやすいため、影を薄くするという手法。
▼グループごとに分けたシャドウマップ
▼分散が大きいグループが影の境界線になる
詳しい手順は以下の通りである。
まず、シャドウマップを描画する。シャドウマップには深度値(ライトからピクセルまでの距離)と深度値の二乗を描き込む。深度値の二乗は分散を計算するときに使用する。
シャドウマップをダウンサンプリングしながらガウシアンブラーを掛けて、ブロックごとのおおよその平均値を求める。これで局所的な深度値の平均値と、深度値の二乗の平均値を求めることができる。
ガウシアンブラーを掛けたテクスチャをシャドウマップとして、シャドウレシーバーのシェーダーリソースビューに登録する。
シャドウレシーバーで影の計算をする。
ピクセル座標をライトビュースクリーン空間からUV空間に変換して、UV空間の座標がXとYそれぞれ0.0f~1.0fの値におさまっているか調べる。範囲外の影は落とせない。(VSMの機能ではないため詳しい説明は割愛)
ライトビュースクリーン空間でのZ値がシャドウマップに描かれている深度値より大きかったら、ピクセルとライトの間に遮蔽物があるということになり、影を描画する。(デプスシャドウの機能。VSMの機能ではないため詳しい説明は割愛)
チェビシェフの不等式を使ってピクセルに光が届く確率を求める。まずは深度値の分散を求める。分散は「二乗の平均 - 平均の二乗」で計算できる。二乗の平均は、シャドウマップにすでに「深度値の二乗の平均値」が描かれている。平均の二乗は、シャドウマップの「深度値の平均値」を二乗することで求まる。二つの値の差で求めた分散の値をvarianceとする。
ライトを遮っている座標から影までの距離を出すために、ライトビュースクリーン空間でのZ値からシャドウマップに描かれている「深度値の平均値」引いた値を求めてmdとする。
varianceとmdの値を利用して光が届く確率lit_factorを計算する。計算式は以下の通りである。
float lit_factor = variance / (variance + md * md);
varianceとmdとlit_factorの関係性は以下の通りである。
▼ varianceとfit_factorの関係の表。(mdは0.5で固定)
variance 分散 |
variance / (variance + md * md) 計算式 |
lit_factor 光が届く確率 |
---|---|---|
0.1 | 0.1 / (0.1 + 0.5 * 0.5) | 0.285714 |
0.5 | 0.5 / (0.5 + 0.5 * 0.5) | 0.666667 |
1.0 | 1.0 / (1.0 + 0.5 * 0.5) | 0.8 |
3.0 | 3.0 / (3.0 + 0.5 * 0.5) | 0.923077 |
▼ varianceとfit_factorの関係のグラフ。(mdは0.5で固定)
▼ mdとfit_factorの関係の表。(varianceは0.5で固定)
md | variance / (variance + md * md) 計算式 |
lit_factor 光が届く確率 |
---|---|---|
0.1 | 0.5 / (0.5 + 0.1 * 0.1) | 0.980392 |
0.5 | 0.5 / (0.5 + 0.5 * 0.5) | 0.666667 |
1.0 | 0.5 / (0.5 + 1.0 * 1.0) | 0.333333 |
3.0 | 0.5 / (0.5 + 3.0 * 3.0) | 0.0526316 |
▼ mdとfit_factorの関係のグラフ。(varianceは0.5で固定)
variance(分散)が大きくなるとlit_factor(光が届く確率)が増大し、mdが大きくなるとlit_factorが減少する。
つまり、varianceが大きいほど影が薄くなり、小さいほど影が濃くなる。
そして、mdが大きいほど影が濃くなり、小さいほど影が薄くなる。
このゲームはポップな世界観のため、それを表現するためにシェーダーでモデルにアウトラインを描画するようにした。
▼アウトライン描画あり
▼アウトライン描画なし
描画するテクセルと近傍8テクセルの深度値や法線のデータを比べて、差が一定以上ある時はアウトラインを描画する。詳しい手順は以下の通りである。
モデルのプロジェクション空間での深度値と、法線をテクスチャに描き込む。
モデルを描画するときに1.で作ったテクスチャをデータを持ってくる。
今から描画するテクセルの深度値と、近傍8テクセルの深度値の平均、この二つの差の絶対値が一定以上ならアウトラインを描画する。
また、深度値だけだとモデルの凹凸のアウトラインが、深度値の差が小さすぎるため、描画されない。
そのため今から描画するテクセルの法線と、近傍8テクセルのそれぞれの法線とのどれかの内積が一定以下だったらアウトラインを描画する
このゲームの世界観に合うように、シェーディングはリムライトの仕組みを利用して行っている。通常のリムライトとは違いライトの位置や方向は考慮せず、常にカメラから見た時のモデルの輪郭部分の光が強いようにしている。その光はキューブマップを利用して、空のカラーの光が少し映り込むようにしている。
詳しい手順は以下の通りである。
float limPower = pow( 1.0f - abs(viewNormal.z), 5.0f );
1.0f - abs(viewNormal.z)で、カメラ空間での法線のZ成分の絶対値が大きいほど0.0fに、小さいほど1.0fに近づく値になる。その値をpowを使って光の強さを調節している。
スイッチを押すまで触ることのできない透明アイテム。その透明アイテムの表現をただモデルを半透明にして描画するのではなく、モデルを透明にし、アウトラインのみ描画した後、そのアウトラインを更にディザリングすることでユニークな表現にした。
プレイヤーが炎と衝突した時、後方に飛び上がってから重力にしたがって落ちるような動きを入れている。
アイテムを投げるときそのまま真下に投げると床を貫通してしまうため、手前側から回り込んで床の向こう側に行く。
アイテムをその場において、他のアイテムと重なってしまったとき、重なりを解決するために、アイテムがクルクル回りながら横に弾かれる動きをする。
炎:プレイヤーを通さない。水を三回当てると消火できる。
壁:プレイヤーを通さない。稼働を当てると壁が動き出す。
全反転魔方陣:通過すると、ステージにある全てのアイテムの性質が反転する。
スイッチと透明アイテム:スイッチを押すと透明アイテムが実体化し、触れることができるようになる。一定時間で透明に戻り、元の位置に戻る。
以下からは、日本ゲーム大賞2021「アマチュア部門」に応募した「メビリンス」とU-22プログラミング・コンテスト2021に応募した「メビリンス」の比較内容を記述する。
改変前はどのステージでも同じスカイキューブマップを使用していたが、ステージごとにスカイキューブマップを変更するようにした。最初の方のステージでは穏やかな空にし、ステージが進むに連れてより壮大な空に変更し、雰囲気が盛り上がるようにした。
▼改変前のスカイキューブマップ
▼改変後のスカイキューブマップ
▼ステージ1,2
▼ステージ3,4
▼ステージ5,6
▼ステージ7,8
▼ステージ9
スカイキューブマップの変更をより効果的にするためにIBL(イメージベースドライティング [Image-Based Lighting])の機能を追加した。
改変前も、ライティングにスカイキューブマップのカラーの影響を受けていたが、よりきれいにスカイキューブマップのカラーが映り込むような計算にして、更にスカイキューブマップに依って明るさが変化するため明るさもステージごとに調整できるようにした。
IBLの手順は以下の通りである。
まずはCPU側(C++)の処理で、コンスタントバッファを利用して、カメラの視点eyePos、明るさluminance、どのくらい空のカラーの影響を受けるかのIBLRateの値をCPU側からGPU側に渡す。
次にシェーダーリソースビューとしてスカイキューブマップをCUP側からGUP側に渡す。
GPU側(hlsl)の処理で、描画するピクセルのワールド座標posInWorldから視点eyePosへのベクトルfromEyeVを計算する。(fromEyeV = psInWorld - eyePos)
fromEyeVとピクセルの法線normalとhlslのreflect関数を使って、反射ベクトルrefVを求める。(refV = reflect(fromEyeV, normal))
スカイキューブマップのテクスチャをrefVを使ってサンプリングしてカラーを取ってくる。
ただし、普通にカラーを持ってくると、金属のように空のカラーがモデルにそのまま映り込んでしまい、気持ちの悪いモデルになってしまう。そこで、MipMapを利用してぼんやりと空のカラーが映り込むようにした。
▼普通のスカイキューブマップテクスチャを使用
▼MIPレベル12のスカイキューブマップテクスチャを使用
MipMapとは、あらかじめテクスチャの縮小版のテクスチャをいくつか計算し、最適化した画像群のことである。描画する時、描画サイズが大きいときは本来の高解像度のテクスチャが使用され、描画サイズが小さいほど縮小版のテクスチャに差し替えられる。これにより処理負荷を軽減する、という用途で使われる。
▼MIPレベル1のスカイキューブマップテクスチャ
▼MIPレベル7のスカイキューブマップテクスチャ
ステージ1,2のスカイキューブマップ
luminance | IBLRate |
---|---|
1.1 | 0.4 |
▼明るく、映り込みが目立つ空のカラー
ステージ7,8のスカイキューブマップ
luminance | IBLRate |
---|---|
3.0 | 0.8 |
▼暗く、映り込みが目立たない空のカラー
改変前はフォワードレンダリングのみでライティングをしていたが、改変後はディファードレンダリングとフォワードレンダリングを組み合わせたライティングになっている。
フォワードレンダリングは、モデルを描画するときに一緒にライティングも行うレンダリング方法だが、ディファードレンダリングはモデルを描画するときにライティングに必要なデータを書き出し、全部書き出し終わった後でライティングを行う方法だ。これにより、モデルの奥に隠れた見えないモデルの不必要なライティングをしなくて済む。
しかし、ディファードレンダリングでは半透明描画ができない。半透明描画をするにはアルファブレンディングを行わなければならない。だが、ライティングに必要なデータを描き込むときに、アルベドカラーなどはアルファブレンディングできるが、法線などアルファブレンディングできないデータがあり、後にライティングするときにおかしな計算になってしまうためである。
これを解決するために、ディファードレンダリングとフォワードレンダリングを組み合わせたライティングを行う。ディファードレンダリングを行った後、半透明描画をするモデルをフォワードレンダリングで描画する。
詳しい手順は以下の通りである。
まずは、MRT(MultiRenderingTarget)という機能を使い、G-Bufferと呼ばれる複数枚のテクスチャにライティングに必要なデータを書き出していく。このゲームでは、7つのG-Bufferを作成しており、それぞれ、アルベドカラー、法線、ビュー座標系の法線、ワールド座標系の座標、ライトビュープロジェクション座標系の座標、プロジェクション座標計の座標、自己発光色の内容になっている。
作成したG-Bufferを、ディファ―ドライティングを行うスプライトのシェーダーリソースビューに登録する。
G-BufferをUVでサンプリングして、そのピクセルのライティングに必要なデータを取り出して、ライティングを行う。ここでディファードレンダリングの処理は終了
ここからフォワードレンダリングを行う。フォワードレンダリングを行う際、深度ステンシルビューを、G-Bufferを作成した時に作られた深度ステンシルビューに設定する。これにより、後から描画してもモデルの前後関係が正しく描画される。
マジックナンバーとは、ソースコード内に直に書き込まれた数値である。マジックナンバーは書いた本人は、どんな目的の数値なのか分かっても、他の人から見たら何の数値なのかわからない。更に、書いた本人すらも時間が立てば忘れてしまい、何に数値かわらかなくなるかもしれない。
また、同じ目的で同じ数値のマジックナンバーが複数個所ある場合、その数値を変更する時、一つ一つ変更していかなければならない。一部変更し忘れるミスをしてしまう危険性もある。
以上のことからマジックナンバーを使用すると、可読性と保守性を下げてしまう。そのため、マジックナンバーを定数や列挙型に置き換えて、一か所にまとめておく改変を行った。
▼Player.cppの一部のコード
改変前はグローバルな名前空間に直書きだったコードを、改変後ではnamespaceの中に、コードの種類ごとに整理して入れた。名前空間中に入れることで、変数などの識別名の競合を限定的なものにすることができる。自分だけなら、識別名の競合ぐらい気をつけておけば大丈夫だと思うかもしれないが、他の人と作業するする時や、ライブラリを利用するときなどに、競合してしまう可能性が出てくる。
▼GameCamera.hの一部のコード
変数を初期化しなければ、想定外のエラーが出てくる可能性がある。初期化せずに宣言だけをした変数にもアドレスは割り振られ、そのアドレスには、宣言前に使用していたデータが残っている可能性がある。初期化していない変数を使用してしまったときに、何の値か分からないデータを使用することとなり、想定外のエラーが出てくる可能性がある。
改変前も気を付けていたが、初期化を忘れているところがあったため、修正した。
▼Player.hの一部のコード
暗黙の型変換は、どのように変換されるか理解しておけば問題ないように思えるが、ビルドするときに出力ウィンドウに警告が発生する。暗黙の型変換は注意しておかないと大量に発生してしまい、出力ウィンドウを圧迫してしまう。その結果、重要な警告を見逃してしまいやすくなってしまう。
そのため、明示的にキャストを書いたり、関数のオーバーロードを増やしたりして、暗黙の型変換をなくすようにした。
▼暗黙の型変換による警告と警告がでた関数
▼関数のオーバーロードを増やし、暗黙の型変換を防ぐ
一つの関数の処理が長いと、見づらい上に、いろんな機能が一つの関数で行われているため何をやっている関数なのか分かりずらい。処理の長い関数は、処理ごとに細かく分けて関数を分割することで、バグが発生した時も確認する部分が限定でき、可読性と保守性に優れる。
改変前は、気にせず同じ関数内にいろんな処理を書いてしまい100行を超える関数が沢山あったが、改変後は、処理の長い関数は、処理ごとに細かく関数を分割した。
▼Player.cppのStart関数
dynamic_castを使えば比較的安全にダウンキャストを行うことができるが、dynamic_cast事態の処理速度が遅いためあまり使用すべきではない。
そのため、改変前では、dynamic_castでダウンキャストが成功した時にだけその派生クラスにしかないメンバ関数を呼んでいたが、改変後では、基底クラスに何も処理をしない仮想関数を作り、目的の派生クラスだけでオーバーライドを行うことで解決した。
▼Player.cppのStart関数