DirectX11の中でコツコツと作った物理演算システムをまとめてみる。
※初心者の勉強メモです。色々と至らぬ点が多々あります。
実際に出来たもの
とてもシンプル。実装したコライダーも四角と丸のみ。(カプセルコライダーは組み合わせれば出来そう?)坂道など斜めの当たり判定は、当たったかのチェック自体は簡単だったが(説明してくれているサイトが沢山あったため)、そこからの押し出し処理が個人的に大変だった。
衝突したら倒れたりとか回転の処理も作ろうとしたけど、箱が暴走し出したので断念。(いつかリベンジしたい)
動かせるオブジェクトは、上から乗るなどすると動きが大分不安定になる…。
全体の処理の流れ
ざっとした流れは以下の通り。
①PhysicsProcessorで当たり判定が関係ない処理を行う。(基本的な重力、摩擦など)
※ここの摩擦は接地状況などに関係なく常にかかる
②CollisionProcessorで当たり判定を取得。衝突したGameObjectのアドレスの取得や、後の押し出し処理に使うmtv(押し出しベクトル?的な)を計算しておく。
③DynamicsProcessorでは②で取得したmtvなどを用いて位置、速度の補正を行う。(詳しくは後で記述)
基本的には、①動く→②当たる→③めり込んだ分を補正するの流れ。①と③を分けて処理をしないと、せっかく計算したmtvが上手く機能しなくなる。
void ProcessorManager::UpdateSystem()
{
// 物理系の処理
m_PhysicsProcessor.Update();
m_CollisionProcessor.Update();
m_DynamicsProcessor.Update();
// ...
}
// physics_system.cpp [物理演算システム制御]
//
// 重力、摩擦などを
// velocity(速度)に設定し、位置への適用を行う
// collision_system.cpp [衝突判定システム制御]
// dynamics_processor.cpp [衝突演算システム制御]
//
// 衝突情報から位置補正を行う。
// PhysicsProcessor → CollisionProcessor の後に行う。
// 物体の動き、衝突判定、補正 ←ここに当たる。
どんな感じで使うのか
ECSアーキテクチャの考え方を超部分的に流用した、なんちゃってUnityシステムを作っているので、GameObjectを生成→Componentを生成・登録→Componentを使ってProcessor(当たり判定、重力など処理を行う部分。ECSのS(system)にあたる)に登録。
処理ごとにオブジェクトをまとめて行わないと、色々と不具合が発生するのでこういう作りにしてみた。
登録手順
GameObject* GameObjectFactory::CreateBall(MiFloat2 pos, float size, MiFloat4 color)
{
GameObject* ball = new GameObject();
//----------------------------------------------------
// コンポーネント登録
//----------------------------------------------------
TransformComponent* transform = new TransformComponent();
RigidbodyComponent* rigidbody = new RigidbodyComponent();
SpriteRendererComponent* sprite = new SpriteRendererComponent();
CircleColliderComponent* collider = new CircleColliderComponent();
ball->AddComponent(transform);
ball->AddComponent(rigidbody);
ball->AddComponent(sprite);
ball->AddComponent(collider);
transform->SetPosition(pos);
transform->SetSize({ size,size });
rigidbody->SetMass(3.0f);
rigidbody->SetGravityScale(9.8f);
rigidbody->SetFriction(1.0f, 1.0f);
SpriteInterface* ballSp = AssetManager::GetInstance()->GetSpriteFromID("circle");
sprite->SetSpriteAddress(ballSp);
sprite->SetColor(color);
sprite->SetOrderInLayer(7);
collider->SetRadius(size / 2.0f);
ProcessorManager* sm = ProcessorManager::GetInstance();
sm->GetRendererSystem()->Entry(transform, sprite);
sm->GetCollisionDynamicsProcessor()->Entry(transform, collider, rigidbody);
sm->GetPhysicsSystem()->Entry(transform, collider, rigidbody);
sm->GetCollisionSystem()->Entry(transform, collider);
return ball;
}
①PhysicsProcessor
とくに特筆するようなことはない気がする。重力と摩擦をかけるだけ。
後々のDynamicsProcessorでの補正が楽になるように、外力を適用する前の位置(Position)、速度(Velocity)をここで保存している。
physics_processor.cppの更新処理
//===================================================
// 物理演算システムの更新処理
//===================================================
void PhysicsProcessor::Update()
{
// deltaTimeを取得
float deltaTime = FPS::GetInstance()->GetDeltaTime();
for (int i = 0; i < m_cmpList.size(); i++) {
// 使用するComponentのアドレスを取得
TransformComponent* transform = m_cmpList[i].pTraCmp;
RigidbodyComponent* rb = m_cmpList[i].pRbCmp;
if (!transform->GetOwner()->GetActive()) continue;
if (!transform->GetEnable() || !rb->GetEnable())continue;
transform->SetPrevPos(transform->GetPosition());
rb->SetPrevVel(rb->GetVelocity());
//----------------------------------------------------
// rigidbodyに力を加える
//----------------------------------------------------
MiFloat2 vel = rb->GetVelocity();
MiFloat2 prevVel = vel;
// 摩擦
vel.x *= rb->GetFriction().x;
vel.y *= rb->GetFriction().y;
// 重力
// ・重力による下へのvelは、後のDynamicsProcessorで計算に使用するため、
// 着地していても常にかかるようにする。
vel.y += rb->GetMass() * rb->GetGravityScale();
//----------------------------------------------------
// コンポーネントへ適用
//----------------------------------------------------
MiFloat2 pos = transform->GetPosition();
rb->SetVelocity(vel);
transform->SetPosition({ pos.x + vel.x * deltaTime, pos.y + vel.y * deltaTime });
}
}
②CollisionProcessor
登録されているGameObjectの当たり判定をまさかの総当たりで取っている。(作ったゲーム内容的に、画面外のオブジェクトがほぼ存在しないのでこれでも動いた。絶対改修した方が良い)
collision_processor.cppの更新処理
//===================================================
// 衝突判定システムの更新処理
//===================================================
void CollisionProcessor::Update()
{
//----------------------------------------------------
// 衝突相手データをリセット
//----------------------------------------------------
for (int i = 0; i < m_cmpList.size(); i++) {
ColliderComponent* collider = m_cmpList[i].pColCmp;
collider->SaveOppData();
}
//----------------------------------------------------
// 衝突判定
//----------------------------------------------------
for (int i = 0; i < m_cmpList.size(); i++) {
TransformComponent& transformA = *m_cmpList[i].pTraCmp;
ColliderComponent* colliderA = m_cmpList[i].pColCmp;
if (!transformA.GetOwner()->GetActive())continue;
if (!transformA.GetEnable() || !colliderA->GetEnable())continue;
for (int j = 0; j < m_cmpList.size() - (i + 1); j++) {
TransformComponent& transformB = *m_cmpList[j + (i + 1)].pTraCmp;
ColliderComponent* colliderB = m_cmpList[j + (i + 1)].pColCmp;
if (!transformB.GetOwner()->GetActive()) continue;
if (!transformB.GetEnable() || !colliderB->GetEnable())continue;
// 静的なオブジェクト同士の場合、判定処理を行わない
if (colliderA->GetStaticState()&& colliderB->GetStaticState() ) {
continue;
}
// LayerMaskがfalseの場合、判定処理を行わない
if (!COLLISION_LAYER_MASK[(int)colliderA->GetLayer()][(int)colliderB->GetLayer()]) {
continue;
}
//----------------------------------------------------
// BoxCollider同士
//----------------------------------------------------
if (colliderA->GetColliderType() == ColliderComponent::Type::Box &&
colliderB->GetColliderType() == ColliderComponent::Type::Box) {
// BoxColliderアドレスに
BoxColliderComponent* bColA = colliderA->GetCollider<BoxColliderComponent>();
BoxColliderComponent* bColB = colliderB->GetCollider<BoxColliderComponent>();
MiFloat2 mtvA, mtvB;
mtvA = mtvB = { 0.0f,0.0f };
// 衝突判定
if (CheckCollision_Sat_BB(*bColA, transformA, *bColB, transformB, &mtvA, &mtvB)) {
colliderA->AddOppData(colliderB, mtvA);
colliderB->AddOppData(colliderA, mtvB);
}
}
}
}
当たり判定の取得方法と、mtv(押し出しベクトル)の計算方法
分離軸判定による当たり判定を作成した。計算についてはこちらのサイト↓を参考にさせていただきました。とても分かりやすかった。
http://marupeke296.com/COL_3D_No13_OBBvsOBB.html
ある1つの軸に2オブジェクトの半径と間(このコードで言うdiffにあたる)を投射して比較するというやり方。要は 1方向から見た時に2オブジェクトが重なっているか? を判定しているだけ。
この重なり具合が、そのままmtv(押し出しベクトル)にあたる。
分離軸判定による当たり判定
bool CheckCollision_Sat_BB(
BoxColliderComponent colA, TransformComponent transA,
BoxColliderComponent colB, TransformComponent transB,
MiFloat2* pMtvA, MiFloat2* pMtvB)
{
/* 法線ベクトルを取得 */
MiFloat2 bDirectA[2];
CalcDirect_Box(bDirectA, colA, transA);
MiFloat2 bDirectB[2];
CalcDirect_Box(bDirectB,colB, transB);
/* 長さを取得 */
float bLengthA[2];
CalcLength_Box(bLengthA, colA);
float bLengthB[2];
CalcLength_Box(bLengthB, colB);
// 分離軸のリスト(A、Bそれぞれの軸2つずつ)
MiFloat2 list_L[4] = { bDirectA[0],bDirectA[1],bDirectB[0],bDirectB[1] };
// コライダーのワールド座標
MiFloat2 worldPosA = GetColliderWorldPos(transA.GetPosition(), transA.GetRot(), colA.GetAnchor());
MiFloat2 worldPosB = GetColliderWorldPos(transB.GetPosition(), transB.GetRot(), colB.GetAnchor());
// A、Bの位置の差
MiFloat2 diff = {
worldPosA.x - worldPosB.x,
worldPosA.y - worldPosB.y };
float minOverlap = 1000.0f;
MiFloat2 minAxis = { 0.0f,0.0f };
for (int i = 0; i < 4; i++) {
// 分離軸
MiFloat2 L = list_L[i];
// 中心点間の距離を投影
float interval;
interval = MiMath::FloatAbs(MiMath::DotFloat2(diff, L));
// 半径を投影
float ra, rb;
ra = MiMath::FloatAbs(MiMath::DotFloat2(bDirectA[0], L) * bLengthA[0]) + MiMath::FloatAbs(MiMath::DotFloat2(bDirectA[1], L) * bLengthA[1]);
rb = MiMath::FloatAbs(MiMath::DotFloat2(bDirectB[0], L) * bLengthB[0]) + MiMath::FloatAbs(MiMath::DotFloat2(bDirectB[1], L) * bLengthB[1]);
// 衝突判定
if (interval > ra + rb) {
// 衝突していない分離軸が1つ以上あるので、ここで処理を終了
return false;
}
/* 最小の重なりを計算 */
if (minOverlap > (ra + rb) - interval) {
minOverlap = (ra + rb) - interval;
minAxis = L;
}
}
/* mtvを設定 */
if (MiMath::DotFloat2(diff, minAxis) < 0) { // colAから見て正しい押し出しベクトルになるよう調整
minAxis.x = -minAxis.x;
minAxis.y = -minAxis.y;
}
*pMtvA = { minAxis.x * minOverlap,minAxis.y * minOverlap };
*pMtvB = { -minAxis.x * minOverlap,-minAxis.y * minOverlap };
return true; // 衝突している
}
// 分離軸の方向を取得
void CalcDirect_Box(MiFloat2 calcDirect[2],BoxColliderComponent col, TransformComponent trans) {
// ベースになる方向ベクトル
MiFloat2 oldDirect[2];
oldDirect[0] = { 1.0f,0.0f };
oldDirect[1] = { 0.0f,1.0f };
// 回転処理
float rot = trans.GetRot() + col.GetLocalRot();
for (int i = 0; i < 2; i++) {
calcDirect[i].x = oldDirect[i].x * cosf(rot) - oldDirect[i].y * sinf(rot);
calcDirect[i].y = oldDirect[i].x * sinf(rot) + oldDirect[i].y * cosf(rot);
}
}
// 分離軸の長さを取得
void CalcLength_Box(float calcLength[2],BoxColliderComponent col) {
calcLength[0] = col.GetSize().x / 2.0f;
calcLength[1] = col.GetSize().y / 2.0f;
}
円同士の当たり判定は、2オブジェクト間の距離と半径の合計を比べればいいだけ。
円形コライダー同士の当たり判定
//===================================================
// 円と円の判定
//===================================================
bool CheckCollision_CC(
CircleColliderComponent colA, TransformComponent transA,
CircleColliderComponent colB, TransformComponent transB,
MiFloat2* pMtvA, MiFloat2* pMtvB) {
// コライダーのワールド座標
MiFloat2 colWorldPosA = GetColliderWorldPos(transA.GetPosition(), transA.GetRot(), colA.GetAnchor());
MiFloat2 colWorldPosB = GetColliderWorldPos(transB.GetPosition(), transB.GetRot(), colB.GetAnchor());
// コライダー間の距離
float diffX = MiMath::FloatAbs(colWorldPosA.x - colWorldPosB.x);
float diffY = MiMath::FloatAbs(colWorldPosA.y - colWorldPosB.y);
float interval = sqrt(diffX * diffX + diffY * diffY);
// 衝突判定
if (interval <= colA.GetRadius() + colB.GetRadius()) {
float overlap = interval - (colA.GetRadius() + colB.GetRadius());
MiFloat2 mtv
= MiMath::NormalizeFloat2({ colWorldPosA.x - colWorldPosB.x ,colWorldPosA.y - colWorldPosB.y });
*pMtvA = { mtv.x,mtv.y };
*pMtvB = { -mtv.x,-mtv.y };
return true;
}
return false;
}
円と四角の当たり判定は、四角コライダーを一度角度が0.0fの状態に戻してからAABBで判定をする。
::: details 円形コライダー、四角コライダーの当たり判定
//===================================================
// AABBと円の判定
//===================================================
bool CheckCollision_BC(
BoxColliderComponent boxCol, TransformComponent boxTrans,
CircleColliderComponent cirCol, TransformComponent cirTrans,
MiFloat2* pBoxMtv, MiFloat2* pCirMtv)
{
// コライダーのワールド座標
MiFloat2 boxWorldPos = GetColliderWorldPos(boxTrans.GetPosition(), boxTrans.GetRot(), boxCol.GetAnchor());
MiFloat2 cirWorldPos = GetColliderWorldPos(cirTrans.GetPosition(), cirTrans.GetRot(), cirCol.GetAnchor());
//----------------------------------------------------
// BoxColliderをAABBとして判定するため、CircleColliderの相対位置を計算
//----------------------------------------------------
MiFloat2 toCircle = {
cirWorldPos.x - boxWorldPos.x,
cirWorldPos.y - boxWorldPos.y };
MiFloat2 cirLocal; // CircleColliderの相対位置
float revRad = -(boxTrans.GetRot() + boxCol.GetLocalRot()); // 逆回転
cirLocal.x = toCircle.x * cosf(revRad) - toCircle.y * sinf(revRad);
cirLocal.y = toCircle.x * sinf(revRad) + toCircle.y * cosf(revRad);
//----------------------------------------------------
// 最近接点(BoxColliderの表面の内、円の中心に最も近い点)を算出
//----------------------------------------------------
MiFloat2 boxColHalf = { boxCol.GetSize().x / 2.0f,boxCol.GetSize().y / 2.0f };
MiFloat2 closet;
closet.x = MiMath::FloatClamp(cirLocal.x, -boxColHalf.x, boxColHalf.x);
closet.y = MiMath::FloatClamp(cirLocal.y, -boxColHalf.y, boxColHalf.y);
//----------------------------------------------------
// 円の方程式により、求めた近接点が円の中に含まれているかを判定
//----------------------------------------------------
MiFloat2 diff = { cirLocal.x - closet.x,cirLocal.y - closet.y };
float dist2 = diff.x * diff.x + diff.y * diff.y;
float radius = cirCol.GetRadius();
if (dist2 < radius * radius) {
float dist = sqrt(dist2);
float overlap = radius - dist;
// 相対的なmtvを計算
MiFloat2 localMtv = MiMath::NormalizeFloat2(diff);
localMtv.x *= overlap;
localMtv.y *= overlap;
// mtvを回転
MiFloat2 mtv;
mtv.x = localMtv.x * cosf(-revRad) - localMtv.y * sinf(-revRad);
mtv.y = localMtv.x * sinf(-revRad) + localMtv.y * cosf(-revRad);
// mtvを設定
*pCirMtv = { mtv.x,mtv.y };
*pBoxMtv = { -mtv.x,-mtv.y };
return true;
}
return false; // 衝突していない
}
※今回作ったゲームでは、GameObject本体の回転(transform.GetRot())とは別に、Colliderの回転(collider.GetLocalRot())とか、GameObjectの中心座標から見たColliderの中心座標のズレとかのパラメータがあるので、引用元より少し面倒くさい。
(といっても法線ベクトルの方向と、コライダーの中心座標あたりの話以外はほぼいっしょ)
※DirectX Math にも内積計算の関数はあるが、今回は学びも兼ねて自作ベクタークラスを使っているので、MiMath::から始まる自作計算関数を使っています。(簡単な絶対値の計算や内積の計算をしてるだけ)
mtv(押し出しベクトル)の計算部分について
該当するのは以下の部分。より小さい力で重なりを解消できるように動きたいので、最も小さい重なりのサイズと、その時の分離軸を保持しておく。
/* 最小の重なりを計算 */
if (minOverlap > (ra + rb) - interval) {
minOverlap = (ra + rb) - interval;
minAxis = L;
}
}
/* mtvを設定 */
if (MiMath::DotFloat2(diff, minAxis) < 0) { // colAから見て正しい押し出しベクトルになるよう調整
minAxis.x = -minAxis.x;
minAxis.y = -minAxis.y;
}
*pMtvA = { minAxis.x * minOverlap,minAxis.y * minOverlap };
*pMtvB = { -minAxis.x * minOverlap,-minAxis.y * minOverlap };
return true; // 衝突している
}
衝突情報を保存する用の構造体
ColliderComponentに衝突情報を保存するようにしてみた。衝突相手のアドレスと、mtvの他、今当たっているか、前当たっていたかのフラグを用意したので、UnityっぽくOnCollisionEnter,Stay,Exitが作れる。
struct OppColliderData { // 衝突情報
ColliderComponent* oppCollider;
MiFloat2 mtv;
bool isCollisionNow;
bool wasCollisionPrev;
bool OnCollisionEnter() const { return isCollisionNow && !wasCollisionPrev; }
bool OnCollisionStay() const { return isCollisionNow; }
bool OnCollisionExit() const { return !isCollisionNow && wasCollisionPrev; }
};
③DynamicsProcessor
②でゲットしたmtvを用いて押し出しを行う。が、そのまま適用すると↓のようにボヨンボヨンするようになったので色々調整。(これはこれで楽しいけども)
以下が全体の流れ。
DynamicsProcessor全体の流れ
//===================================================
// 物理演算システムの更新処理
//===================================================
void DynamicsProcessor::Update()
{
// deltaTimeを取得
float deltaTime = FPS::GetInstance()->GetDeltaTime();
for (int i = 0; i < m_cmpList.size(); i++) {
// 使用するComponentのアドレスを取得
TransformComponent* transform = m_cmpList[i].pTraCmp;
ColliderComponent* collider = m_cmpList[i].pColCmp;
RigidbodyComponent* rb = m_cmpList[i].pRbCmp;
if (!transform->GetOwner()->GetActive())continue;
if (!transform->GetEnable() || !collider->GetEnable() || !rb->GetEnable())continue;
//----------------------------------------------------
// 衝突時の物体の動き
//----------------------------------------------------
MiFloat2 pos = transform->GetPosition();
MiFloat2 revVel = { 0.0f,0.0f }; // 外力
bool isGround = false;
{
MiFloat2 maxMtv = { 0.0f,0.0f };
MiFloat2 minMtv = { 0.0f,0.0f };
// 最も大きいmtv,小さいmtvを保持
const std::vector<ColliderComponent::OppColliderData>& oppData = collider->GetOppData();
for (int i = 0; i < oppData.size(); i++) {
ColliderComponent::OppColliderData data = oppData[i];
if (data.oppCollider == nullptr) continue;
if (!data.isCollisionNow) continue;
// xのmtv
if (maxMtv.x < data.mtv.x && data.mtv.x > MTV_ROUND_DOWN) maxMtv.x = data.mtv.x;
else if (minMtv.x > data.mtv.x && data.mtv.x < -MTV_ROUND_DOWN) minMtv.x = data.mtv.x;
// yのmtv
if (maxMtv.y < data.mtv.y && data.mtv.y > MTV_ROUND_DOWN) maxMtv.y = data.mtv.y;
else if (minMtv.y > data.mtv.y && data.mtv.y < -MTV_ROUND_DOWN) minMtv.y = data.mtv.y;
}
// 位置にそのままmtvを適用
pos.x += minMtv.x + maxMtv.x;
pos.y += minMtv.y + maxMtv.y;
// velにも加える(後の*scaledDeltaTimeで影響を受けないように除算しておく)
revVel.x += (minMtv.x + maxMtv.x) / deltaTime * MTV_FORCE_SCALE;
revVel.y += (minMtv.y + maxMtv.y) / deltaTime * MTV_FORCE_SCALE;
//----------------------------------------------------
// 剛体同士の接触での振動を防ぐ処理
// ・相反する2方向から力がかかっている時、
// なるべく動きをリセットするようにする
//----------------------------------------------------
{
// 力の合計値が一定未満の場合に限る(片方の値が大きすぎる場合はそちらを優先するため)
bool notUseDunamics
= minMtv.y < 0.0f && maxMtv.y > 0.0f
&& MiMath::FloatAbs(minMtv.y + maxMtv.y) < 5.0f;
if (notUseDunamics) {
revVel.y = 0.0f;
pos.y = transform->GetPrevPos().y;
}
}
//----------------------------------------------------
// 接地判定
//----------------------------------------------------
if (minMtv.y < ON_GROUND_MTV) {
isGround = true;
}
}
//----------------------------------------------------
// 速度が急激に変化するのを抑える
//----------------------------------------------------
revVel.x = MiMath::FloatClamp(revVel.x, -CHANGE_RATE_MAX, CHANGE_RATE_MAX);
revVel.y = MiMath::FloatClamp(revVel.y, -CHANGE_RATE_MAX, CHANGE_RATE_MAX);
//----------------------------------------------------
// コンポーネントへ適用
//----------------------------------------------------
rb->SetIsGround(isGround);
rb->SetVelocity({ rb->GetVelocity().x + revVel.x,rb->GetVelocity().y + revVel.y });
transform->SetPosition({ pos.x + revVel.x * deltaTime, pos.y + revVel.y * deltaTime });
//----------------------------------------------------
// 動きがあまりに小さかった場合、位置を1フレーム前のものへリセット
// ・振動するのを防ぐ
//----------------------------------------------------
{
if (MiMath::FloatAbs(transform->GetPrevPos().x - transform->GetPosition().x) < VIBRATION_RATE_X) {
// 位置を1フレーム前に戻す
transform->SetPosition({ transform->GetPrevPos().x,transform->GetPosition().y });
// 速度がほぼ0になるように
float resetVel = MiMath::FloatClamp(rb->GetVelocity().x, -VIBRATION_CLAMP, VIBRATION_CLAMP);
rb->SetVelocity({ resetVel,rb->GetVelocity().y});
}
if (MiMath::FloatAbs(transform->GetPrevPos().y - transform->GetPosition().y) < VIBRATION_RATE_Y) {
// 位置を1フレーム前に戻す
transform->SetPosition({ transform->GetPosition().x , transform->GetPrevPos().y });
// 速度がほぼ0になるように
float resetVel = MiMath::FloatClamp(rb->GetVelocity().y, -VIBRATION_CLAMP, VIBRATION_CLAMP);
rb->SetVelocity({ rb->GetVelocity().x,resetVel });
}
}
}
}
(このDynamicsProcessorに至ってはほぼオリジナル。色々と不足した処理や変な考え方があるかもしれないが、とりあえず細かく書き残しておく)
①mtvの適用
まず、②で得た衝突情報を使ってmtvの最大値、最小値を取得する。(最大値、最小値とは言うが、意味合い的には相反する方向の最大値同士の方が正しい。右と左、上と下)
mtv(押し出しベクトル)は現在位置からどれぐらい動けば当たらない位置にいけるか?的な値なので、そのまま位置に適用すればめり込まずに済む。
ただ、加えてこのゲームにはvelocity(速度)が存在するので、そちらにも上手く適用しないと 「位置は補正されるのに速度だけ上がっていく……」 みたいな事態が発生する。ので、deltaTimeを除算して、いい感じの定数を乗算したところでvelocityにも加えておく。
※ちなみに、最大値、最小値ではなく取得したmtv全てを適用させると、床ブロック同士の間に立っている時(床との当たり判定が2つ)、単純計算でmtvが2倍になるので挙動が大変なことになる。(そりゃそうだ)
{
MiFloat2 maxMtv = { 0.0f,0.0f };
MiFloat2 minMtv = { 0.0f,0.0f };
// 最も大きいmtv,小さいmtvを保持
// ...割愛
// 位置にそのままmtvを適用
pos.x += minMtv.x + maxMtv.x;
pos.y += minMtv.y + maxMtv.y;
// velにも加える(後の*scaledDeltaTimeで影響を受けないように除算しておく)
// MTV_FORCE_SCALE は 0.3f
revVel.x += (minMtv.x + maxMtv.x) / deltaTime * MTV_FORCE_SCALE;
revVel.y += (minMtv.y + maxMtv.y) / deltaTime * MTV_FORCE_SCALE;
}
②剛体同士の接触での振動を防ぐ処理(主にY方向)
ボヨンボヨンするのを防ぐ処理。そもそもボヨンボヨンする原因はぶつかり合ったオブジェクト同士(仮にABとする)の、それぞれにかかるmtvが 「AのBに対するmtv < BのAに対するmtv」→「AのBに対するmtv > BのAに対するmtv」→「AのBに対するmtv < BのAに対するmtv」のように上下し続けるから。
なので、相対するmtvがあるかつ、その合計値が一定未満(mtvを適用してもそこまで大きな動きにならない)の場合は動きをリセットする。
{
// 力の合計値が一定未満の場合に限る(片方の値が大きすぎる場合はそちらを優先するため)
bool notUseDunamics
= minMtv.y < -0.001f && maxMtv.y > 0.001f
&& MiMath::FloatAbs(minMtv.y + maxMtv.y) < 5.0f;
if (notUseDunamics) {
revVel.y = 0.0f;
pos.y = transform->GetPrevPos().y;
}
}
以下が比較。
[上]この処理がないver(ボヨンボヨンする)
[下]がこの処理があるver(前よりは安定してはいる。振動が抑えられはする)
(しかし、無いときと比べて動きは安定したが、これを使ってアクションゲーム作れる?と言われたら、うーん……。という感じ。もう少し考えていきたい)
③接地判定
④範囲外処理
⑤適用処理
あんまり語ることがない処理群。そのままの意味。
// 地面に乗っているとするmtv値
#define ON_GROUND_MTV (-0.05f)
// 変化値の最大・最小
#define CHANGE_RATE_MAX (700.0f)
//----------------------------------------------------
// 接地判定
//----------------------------------------------------
if (minMtv.y < ON_GROUND_MTV) {
isGround = true;
}
//----------------------------------------------------
// 速度が急激に変化するのを抑える
//----------------------------------------------------
revVel.x = MiMath::FloatClamp(revVel.x, -CHANGE_RATE_MAX, CHANGE_RATE_MAX);
revVel.y = MiMath::FloatClamp(revVel.y, -CHANGE_RATE_MAX, CHANGE_RATE_MAX);
//----------------------------------------------------
// コンポーネントへ適用
//----------------------------------------------------
rb->SetIsGround(isGround);
rb->SetVelocity({ rb->GetVelocity().x + revVel.x,rb->GetVelocity().y + revVel.y });
transform->SetPosition({ pos.x + revVel.x * deltaTime, pos.y + revVel.y * deltaTime });
⑥振動防止処理
目的で言えば②と似ているが、もっとざっくりとしている。
全ての処理を終わらせたのち、処理前と処理後の位置の変化があまりに小さかった場合、移動しなかった、静止していたと考える。
位置を1フレーム前に戻した上で、速度も0に近づける。(今回の処理では床との接地判定を、下から上に対するmtvで取っているので、速度を完全に0にすると挙動がおかしくなる。静止してはいるが、あくまで0に近づけるだけにしておく)
//----------------------------------------------------
// 動きがあまりに小さかった場合、位置を1フレーム前のものへリセット
// ・振動するのを防ぐ
//----------------------------------------------------
{
if (MiMath::FloatAbs(transform->GetPrevPos().x - transform->GetPosition().x) < VIBRATION_RATE_X) {
// 位置を1フレーム前に戻す
transform->SetPosition({ transform->GetPrevPos().x,transform->GetPosition().y });
// 速度を0に近づける
float resetVel = MiMath::LerpFloat(rb->GetVelocity().x, 0.0f, 0.5f);
rb->SetVelocity({ resetVel,rb->GetVelocity().y});
}
if (MiMath::FloatAbs(transform->GetPrevPos().y - transform->GetPosition().y) < VIBRATION_RATE_Y) {
// 位置を1フレーム前に戻す
transform->SetPosition({ transform->GetPosition().x , transform->GetPrevPos().y });
// 速度を0に近づける
float resetVel = MiMath::LerpFloat(rb->GetVelocity().y, 0.0f, 0.5f);
rb->SetVelocity({ rb->GetVelocity().x,resetVel });
}
}
最終的なコードを見ると大分シンプルな処理だが、震える箱と格闘しながら、何をすれば動作が安定するのか相当考えていた。気がする。
(こうすると劇的ビフォーアフター!に見えてくる)
[上]before
おまけ:回転もやりたかった
現行の処理では、箱が坂道を下ってもコードで指定した角度のまま。当たった場所、スピードとかでいい感じに回転させてみたかった。
大失敗した。色々と読んだり調べたりしてみたが、さっぱり……。
以下は色んなところからコピペしたり、公式を見たりして書いたつぎはぎコード。(何が何の処理なのかもよく覚えていない。自身への戒めとして残しておく)。動かすと上のようになる。
角速度についてはいずれリベンジしたい。
//----------------------------------------------------
// 角運動量を求める
//----------------------------------------------------
{
float angularMomentNum = rb->GetAngularMomentNum();
// mtvの逆数を位置ベクトル(物体中心から見た回転中心の相対位置)として考える
DirectX::XMFLOAT2 r = { (minMtv.x + maxMtv.x) * -1.0f,(minMtv.y + maxMtv.y) * -1.0f };
// rの範囲外処理(transformの大きさで仮の処理。要修正)
r.x = MiMath::FloatClamp(r.x, -transform->GetSize().x/2.0f , transform->GetSize().x/2.0f );
r.y = MiMath::FloatClamp(r.y, -transform->GetSize().y/2.0f , transform->GetSize().y/2.0f );
float mass = 1.0f;
DirectX::XMFLOAT2 p = {mass * rb->GetVelocity().x/deltaTime ,mass * rb->GetVelocity().y /deltaTime};
// 角運動量L
float L = MiMath::CrossFloat2(r, p);
// 角運動量を更新
angularMomentNum *= 0.99f;
angularMomentNum += L;
float inertia = (mass / 12.0f) * (transform->GetSize().x*transform->GetSize().x + transform->GetSize().y * transform->GetSize().y);
float angularVelocity = angularMomentNum / inertia;
// transformの回転に適用
transform->SetRotPivot(r, transform->GetRot() + angularVelocity * deltaTime);
rb->SetAngularMomentNum(angularMomentNum);
}
最後に
色々と大変だったが、色々と機能を作っておいたおかげでゲーム作りでは非常に役に立った。
ただ、物理演算マシマシのアクションゲームが作れる出来ではないので、これからも発展させていきたい。
参考
http://marupeke296.com/COL_main.html
非常にお世話になりました。OBB以外にも必要な知識がたくさん載ってる。内積、外積についても解説してくれているのが個人的にありがたかったです。