背景
今や、個人でゲーム開発ができるような時代となっており、その開発プラットフォームも様々なものが台頭してきています。最先端なエンジンなどが注目されている一方で、逆に昔ながらのExcelでゲームを作ってみる人もいる現代。いちMATLABファンとして、MATLABでゲームを作ってやろうぜ!と思う人も少なからずいらっしゃるかと思います。
私もそんなひとりだったのですが、とあるアクションゲームを作ろうとしたとき、早速とある壁にぶつかりました。それが衝突判定です。今回は、MATLABで衝突判定をなんとかしようと頑張ったその過程をご紹介できればと思います。飽くまでなんちゃってなので、ツッコミどころは多々あるかと思いますが、何卒ご容赦ください。
問題設定
キーボード操作で、自キャラを動かす見下ろし型2Dゲームを考えます。
例えばロックマンエグゼのように、キャラ位置や移動量が離散的なものだった場合、恐らく話はそこまで難しくなかったかもしれません。各マスに移動可能かどうかという情報を付与しておいて、移動しようとした先が移動不可だった場合はその移動をキャンセルすればよいのですから。
↓イメージ図(https://www.youtube.com/watch?v=wpBVOjabGwk)

しかし今回は、ゼルダの伝説のようにキャラクターの移動が連続的な仕様を考えます。このとき、自キャラの位置は必ずしもマス目のキリの良い位置とは限りませんし、どれくらい移動するかもプレイヤーがどれくらいの時間キーを入力したかに依るので、上述した仕様は恐らく使えないかと思います。移動した先が移動不可であることの判定と、移動不可だった場合にどの程度その移動をキャンセルすればよいかの計算にひと工夫必要そうです。
↓イメージ図(https://www.blogodisea.com/the-legend-of-zelda-para-nes-nintendo.html)

最初に考えた仕様
ざっくり話すと、自キャラ及び障害物をrectangle等ではなくてpolyshapeとして描画し、polyshapeの重なり(交差)を抽出するintersectを使用して、①衝突するような移動かどうか ②どの程度障害物にめり込むか の2つを判定/計算するような方法です。
↓コード:実行後に矢印キー操作で小さい四角が動きます。
clear
close
clc
fig = figure;
ax = axes('Parent',fig);
polyObs = polyshape([0 0 5 5], [0 2 2 0]);
polyEgo = polyshape([2 2 3 3], [3 4 4 3]);
objObs = plot(ax, polyObs);
hold(ax, "on");
objEgo = plot(ax, polyEgo);
xlim(ax, [0 5]);
ylim(ax, [0 5]);
grid(ax, "on");
axis(ax, "equal");
fig.KeyPressFcn = @(src,event) myFunction(src, event, polyObs, objEgo);
%%
function myFunction(src, event, polyObs, objEgo)
% 初期化
polyCurrent = objEgo.Shape;
vel = 0.5;
% 移動方向/量の決定
switch(event.Key)
case 'rightarrow'
vx = vel;
vy = 0;
case 'leftarrow'
vx = -vel;
vy = 0;
case 'uparrow'
vx = 0;
vy = vel;
case 'downarrow'
vx = 0;
vy = -vel;
end
% 自機の移動
polyMoved = translate(polyCurrent, [vx, vy]);
% 移動後の自機と障害物との重なりを判定
polyInter = intersect(polyObs, polyMoved);
% 補正としてのシフト量を算出
if isempty(polyInter.Vertices)
% 移動後の自機と障害物が重なっていない場合はシフトさせない
shiftX = 0;
shiftY = 0;
else
% 移動後の自機と障害物が重なっていた場合は、重なった量だけシフトさせる
shiftX = max(polyInter.Vertices(:,1)) - min(polyInter.Vertices(:,1));
shiftY = max(polyInter.Vertices(:,2)) - min(polyInter.Vertices(:,2));
end
% シフト量を踏まえて、改めて自機を移動させる
objEgo.Shape = translate(objEgo.Shape, [vx - shiftX*sign(vx), vy - shiftY*sign(vy)]);
end
正直、これでもそれなりに判定できるかと思います。
ただし、この仕様で問題視したのは、移動量が障害物や自キャラサイズに相対して小さくないと成り立たないという点です。要するに、補正量は最大でも自キャラサイズにしかならないので、自キャラがまるごと障害物にめり込むような移動が発生した場合に、その移動を補正しきれないのです。
そんな移動あるの?って点はさておき…。現状想定しているのはダメージ時のノックバックとかですかね…?
改良版
ということで、次のように修正してみました。
変更点としてはシフト量算出の部分で、ver.1では自キャラと障害物が重なった部分のサイズを補正量としていましたが、ver.2では移動前のpolyshapeから移動後のpolyshapeへ向けて引いた線分を使用して補正量を算出しています。
clear
close
clc
fig = figure;
ax = axes('Parent',fig);
polyObs = polyshape([0 0 5 5], [0 2 2 0]);
polyEgo = polyshape([2 2 3 3], [3 4 4 3]);
objObs = plot(ax, polyObs);
hold(ax, "on");
objEgo = plot(ax, polyEgo);
xlim(ax, [0 5]);
ylim(ax, [0 5]);
grid(ax, "on");
axis(ax, "equal");
fig.KeyPressFcn = @(src,event) myFunction(src, event, polyObs, objEgo);
%%
function myFunction(src, event, polyObs, objEgo)
% 初期化
polyCurrent = objEgo.Shape;
vel = 0.5;
shiftX = 0;
shiftY = 0;
% 移動方向/量の決定
switch(event.Key)
case 'rightarrow'
vx = vel;
vy = 0;
case 'leftarrow'
vx = -vel;
vy = 0;
case 'uparrow'
vx = 0;
vy = vel;
case 'downarrow'
vx = 0;
vy = -vel;
end
% 自機の移動
polyMoved = translate(polyCurrent, [vx, vy]);
% 移動後の自機と障害物との重なりを判定
polyInter = intersect(polyObs, polyMoved);
% 補正としてのシフト量を算出
N = numsides(polyInter);
% polyMovedと障害物がめり込まず接しているだけの場合はシフトさせない
% めり込み or 接触 の判定は交差したpolyshapeが多角形かどうか。線でしか交差しない場合はN=0になる。
if N > 0
for i = 1:4
% 四角の頂点ごとに、移動前から移動後に向けて線分を引き、その線分と障害物が重なった分だけシフトする。
% 頂点ごとの調停としては一番長い線分を採用し、その線分のX, Yだけシフトする
lineSeg = [polyCurrent.Vertices(i,1), polyCurrent.Vertices(i,2) ;
polyMoved.Vertices(i,1), polyMoved.Vertices(i,2)];
[in, ~] = intersect(polyObs, lineSeg);
if ~isempty(in)
vec = [shiftX, in(2,1)-in(1,1)];
[~, idx] = max(abs(vec));
shiftX = vec(idx);
vec = [shiftY, in(2,2)-in(1,2)];
[~, idx] = max(abs(vec));
shiftY = vec(idx);
end
end
end
% シフト量を踏まえて、改めて自機を移動させる
objEgo.Shape = translate(objEgo.Shape, [vx-shiftX, vy-shiftY]);
end
これにより、自キャラが丸ごとめり込むような移動に対しても補正が可能になりました。
まとめ
polyshapeを使用して、MATLAB上で簡易的な衝突判定と補正を行う方法を紹介しました。
今回、初めてpolyshapeに触りましたが、いろいろな関数が用意されてる上に、Excelで座標点をインポートして一気に障害物を作成する方法もあるので、中々便利だと感じました。
衝突判定のためにわざわざpolyshapeを描画する必要は生じますが、polyshapeにもAlphaDataのプロパティがあり透明度を設定できるので、見映え上の影響はゼロのはずです。
これが組み込まれたゲームについては追って記事を作成予定ですので、気長にお待ちくだされば幸いです。

