概要
MATLABでドット絵のシーケンスを組んで、モーションのようなものを作成してみました。
Image関数を使って画像をグラフィックオブジェクトとしてaxes上に描画し、そのCDataを適宜切り替えてモーションのように見せています。また、figureのコールバック関数を使用してマウスクリックを判定し、クリック時には攻撃モーションのようなアニメーションを表示します。最後には、カラーパレットをフレームごとに切り替えてダメージエフェクトのようなものも作ってみました。
参考リンク
プログラム構成
今回の記事を書くにあたり、下記記事を参考にさせていただきました。isgraphicsを使用した無限ループだったり、コールバック関数の作成について大変参考になりました。ありがとうございました。
画像元
使用する画像はこちらのサイトのものをお借りしました。ゼルダ作品である理由は私が大好きだからです。いろいろな素材が1枚の画像にまとめられており、非常に使いやすかったです。海外のファンメイドコンテンツってすごい…。
歩きモーション
今回は簡単化のために、正面方向のみを考えます。
原理としては、一定時間ごとに2種類の絵(右足が前の絵と左足が前の絵)を切り替えることで歩行しているように見せます。今回で言うと、次の2つの絵ですね。
↓その①:元画像の左から2~17 pix、上から12~27 pixの範囲にある画像

↓その②:元画像の左から19~34 pix、上から12~27 pixの範囲にある画像

コード
clear
close
clc
fig = figure;
ax = axes(fig);
ax.XLim = [0 3];
ax.YLim = [0 3];
axis(ax, 'equal');
spriteSheet = imread("Playable Characters - Link.png");
obj = image(ax, 'CData',spriteSheet(12:27,2:17,:), 'XData',[1 2], 'YData',[2 1]);
fps = 30;
cntStep = 0;
cntAttack = 0;
cntDamaged = 0;
while(isgraphics(fig))
% 10フレームごとに2種類の絵を切り替える
if cntStep < 10
obj.CData = spriteSheet(12:27,2:17,:);
else
obj.CData = spriteSheet(12:27,19:34,:);
end
% RGBがそれぞれ116のpixを背景とみなして、そこだけ透明度を0にする
isBgArray = obj.CData(:,:,1) == 116 & obj.CData(:,:,2) == 116 & obj.CData(:,:,3) == 116;
obj.AlphaData = ~isBgArray;
% cntStepの更新
cntStep = rem(cntStep+1, 10*2);
pause(1/fps);
end
補足
isBgArrayを使って透明度をわちゃわちゃしてる部分については、こちらの記事で詳細を解説しています。よろしければご覧ください。
実行結果
攻撃モーション
攻撃モーションも考え方は同様です。WindowButtonDownFcnを使用してクリック時にフラグを立てるようにして、フラグが立ったらcntAttackを全体フレームの長さに設定。その後、0で飽和するようにcntAttackを減算しながら、規定のフレーム数になったら絵を切り替えるという仕様になっています。
攻撃については、4種類の絵を使用しました。ここのシーケンスの長さを変えることで、長く振りかぶって重たい攻撃のように見せるとか、いろんな演出が加えられそうですね。
コード
clear
close
clc
fig = figure;
ax = axes(fig);
ax.XLim = [0 3];
ax.YLim = [0 3];
axis(ax, 'equal');
spriteSheet = imread("Playable Characters - Link.png");
obj = image(ax, 'CData',spriteSheet(12:27,2:17,:), 'XData',[1 2], 'YData',[2 1]);
fps = 30;
cntStep = 0;
cntAttack = 0;
cntDamaged = 0;
fig.UserData = struct('isLeftClick',false, 'isRightClick',false); % Callback関数からのデータ受け渡し用
fig.WindowButtonDownFcn = @btnDownFnc; % マウスクリック時に実行されるCallback関数
while(isgraphics(fig))
if fig.UserData.isLeftClick % 攻撃モーション中
%剣の振りはじめ
if cntAttack == 0
cntAttack = 12;
obj.CData = spriteSheet(48:63,2:17,:);
end
%剣を振っている最中
if cntAttack == 9
obj.CData = spriteSheet(48:74,19:34,:);
obj.YData = [2 0.26667];
end
if cntAttack == 6
obj.CData = spriteSheet(48:70,36:51,:);
obj.YData = [2 0.53334];
end
if cntAttack == 3
obj.CData = spriteSheet(48:66,53:68,:);
obj.YData = [2 0.8];
end
%モーション後の初期化
if cntAttack == 1
fig.UserData.isLeftClick = false;
obj.CData = spriteSheet(12:27,19:34,:);
obj.YData = [2 1];
end
%cntAttackの更新
cntAttack = max(cntAttack-1, 0);
else% 攻撃モーションでないとき
% 10フレームごとに2種類の絵を切り替える
if cntStep < 10
obj.CData = spriteSheet(12:27,2:17,:);
else
obj.CData = spriteSheet(12:27,19:34,:);
end
% cntStepの更新
cntStep = rem(cntStep+1, 10*2);
end
% RGBがそれぞれ116のpixを背景とみなして、そこだけ透明度を0にする
isBgArray = obj.CData(:,:,1) == 116 & obj.CData(:,:,2) == 116 & obj.CData(:,:,3) == 116;
obj.AlphaData = ~isBgArray;
pause(1/fps);
end
%%
function btnDownFnc(src, data)
switch(data.Source.SelectionType)
case 'normal' %左クリック
src.UserData.isLeftClick = true;
case 'alt' %右クリック
src.UserData.isRightClick = true;
case 'open' %ダブルクリック
src.UserData.isLeftClick = true;
case 'extend' %同時クリック/ホイールクリック
src.UserData.isLeftClick = true;
end
end
実行結果
ダメージエフェクト
せっかくなのでダメージエフェクトまでやってしまいましょう。メインループの最後に、ダメージ中(fig.UserData.isRightClick == true)のときのみ実行されるように処理を追加します。
シーケンスの組み方としては、攻撃モーションの時と同様です。違いとしては、これまでobj.CDataを直接変更していたのに対して、処理の都合でimgSrcという変数を追加したことです。カラーパレットの変更に際して、元画像のどこの部分が何色だったというマップを確保しておくためのバッファのような役割です。これを追加したことで、obj.CDataの色を変えても元画像の色配置を計算できるようになりました。
具体的には、画像の緑色/肌色/茶色の部分をそれぞれ論理配列として持っておいて、cntDamagedごとにそこへ割り当てる色を変えているという処理になっています。
コード
clear
close
clc
fig = figure;
ax = axes(fig);
ax.XLim = [0 3];
ax.YLim = [0 3];
axis(ax, 'equal');
spriteSheet = imread("Playable Characters - Link.png");
imgSrc = spriteSheet(12:27,2:17,:);
obj = image(ax, 'CData',imgSrc, 'XData',[1 2], 'YData',[2 1]);
fps = 30;
cntStep = 0;
cntAttack = 0;
cntDamaged = 0;
fig.UserData = struct('isLeftClick',false, 'isRightClick',false); % Callback関数からのデータ受け渡し用
fig.WindowButtonDownFcn = @btnDownFnc; % マウスクリック時に実行されるCallback関数
while(isgraphics(fig))
if fig.UserData.isLeftClick % 攻撃モーション中
%剣の振りはじめ
if cntAttack == 0
cntAttack = 12;
imgSrc = spriteSheet(48:63,2:17,:);
end
%剣を振っている最中
if cntAttack == 9
imgSrc = spriteSheet(48:74,19:34,:);
obj.YData = [2 0.26667];
end
if cntAttack == 6
imgSrc = spriteSheet(48:70,36:51,:);
obj.YData = [2 0.53334];
end
if cntAttack == 3
imgSrc = spriteSheet(48:66,53:68,:);
obj.YData = [2 0.8];
end
%モーション後の初期化
if cntAttack == 1
fig.UserData.isLeftClick = false;
imgSrc = spriteSheet(12:27,19:34,:);
obj.YData = [2 1];
end
%cntAttackの更新
cntAttack = max(cntAttack-1, 0);
else% 攻撃モーションでないとき
% 10フレームごとに2種類の絵を切り替える
if cntStep < 10
imgSrc = spriteSheet(12:27,2:17,:);
else
imgSrc = spriteSheet(12:27,19:34,:);
end
% cntStepの更新
cntStep = rem(cntStep+1, 10*2);
end
% RGBがそれぞれ116のpixを背景とみなして、そこだけ透明度を0にする
isBgArray = imgSrc(:,:,1) == 116 & imgSrc(:,:,2) == 116 & imgSrc(:,:,3) == 116;
obj.CData = imgSrc;
obj.AlphaData = ~isBgArray;
% ダメージエフェクト
if fig.UserData.isRightClick % ダメージ判定中
isGreen = imgSrc(:,:,1) == 128 & imgSrc(:,:,2) == 208 & imgSrc(:,:,3) == 16; %緑色のpixelを判定する論理配列
isBeige = imgSrc(:,:,1) == 252 & imgSrc(:,:,2) == 152 & imgSrc(:,:,3) == 56; %肌色のpixelを判定する論理配列
isBrown = imgSrc(:,:,1) == 200 & imgSrc(:,:,2) == 76 & imgSrc(:,:,3) == 12; %茶色のpixelを判定する論理配列
if cntDamaged == 0
cntDamaged = 30; %カウンタの初期化
elseif cntDamaged > 1
% ダメージエフェクト(cntごとに、普通→カラーパレット1→パレット2→パレット3→普通を繰り返す)
switch( rem(cntDamaged,4) )
case 0
obj.CData(:,:,1) = isGreen*0 + isBeige*0 + isBrown*216;
obj.CData(:,:,2) = isGreen*0 + isBeige*128 + isBrown*40;
obj.CData(:,:,3) = isGreen*0 + isBeige*136 + isBrown*0;
case 1
% cntDamaged = 0では本methodは実行されないので、静止し続けた場合を想定してcntDamaged = 1を元画像にする
case 2
obj.CData(:,:,1) = isGreen*216 + isBeige*252 + isBrown*252;
obj.CData(:,:,2) = isGreen*40 + isBeige*152 + isBrown*252;
obj.CData(:,:,3) = isGreen*0 + isBeige*56 + isBrown*252;
case 3
obj.CData(:,:,1) = isGreen*0 + isBeige*92 + isBrown*252;
obj.CData(:,:,2) = isGreen*0 + isBeige*148 + isBrown*252;
obj.CData(:,:,3) = isGreen*168 + isBeige*252 + isBrown*252;
end
else
fig.UserData.isRightClick = false;
end
%cntDamagedの更新
cntDamaged = max(cntDamaged-1, 0);
end
pause(1/fps);
end
%%
function btnDownFnc(src, data)
switch(data.Source.SelectionType)
case 'normal' %左クリック
src.UserData.isLeftClick = true;
case 'alt' %右クリック
src.UserData.isRightClick = true;
case 'open' %ダブルクリック
src.UserData.isLeftClick = true;
case 'extend' %同時クリック/ホイールクリック
src.UserData.isLeftClick = true;
end
end
実行結果
余談:敵キャラでも
クリックごとに、出現⇔消滅を繰り返すオクタロックです。
使用した画像
敵キャラ
消滅エフェクト
出現エフェクトは、リンクのSprite Sheetにあった爆弾の爆発エフェクトを使用しました。
コード
clear
close
clc
fig = figure;
ax = axes(fig, "Color",[0.5, 0.5, 0.5]);
ax.XLim = [0 3];
ax.YLim = [0 3];
axis(ax, 'equal');
spriteSheetPC = imread("Playable Characters - Link.png");
spriteSheetEnm = imread("Enemies & Bosses - Overworld Enemies.png");
spriteSheetEfct = imread("Miscellaneous - Enemy Death.png");
imgInit = spriteSheetEnm(12:27,19:34,:);
imgInit(:,:,:) = 116;
obj = image(ax, 'CData',imgInit, 'XData',[1 2], 'YData',[2 1]);
fps = 30;
cntStep = 0;
cntEffect = 0;
isSpawned = false;
fig.UserData = struct('isLeftClick',false); % Callback関数からのデータ受け渡し用
fig.WindowButtonDownFcn = @btnDownFnc; % マウスクリック時に実行されるCallback関数
while(isgraphics(fig))
if fig.UserData.isLeftClick
if isSpawned
% 消滅エフェクト
if cntEffect == 0
cntEffect = 22;
obj.CData = spriteSheetEfct(1:16,49:63,:);
elseif cntEffect > 18
obj.CData = spriteSheetEfct(1:16,33:47,:);
elseif cntEffect > 15
obj.CData = spriteSheetEfct(1:16,17:31,:);
elseif cntEffect > 12
obj.CData = spriteSheetEfct(1:16,1:15,:);
elseif cntEffect > 9
obj.CData = spriteSheetEfct(1:16,17:31,:);
elseif cntEffect > 6
obj.CData = spriteSheetEfct(1:16,33:47,:);
elseif cntEffect > 3
obj.CData = spriteSheetEfct(1:16,49:63,:);
elseif cntEffect == 1
obj.CData = imgInit;
isSpawned = false;
fig.UserData.isLeftClick = false;
end
else
% 出現エフェクト
if cntEffect == 0
cntEffect = 12;
elseif cntEffect > 7
obj.CData = spriteSheetPC(186:201,139:154,:);
elseif cntEffect > 5
obj.CData = spriteSheetPC(186:201,156:171,:);
elseif cntEffect > 3
obj.CData = spriteSheetPC(186:201,173:188,:);
elseif cntEffect == 1
obj.CData = imgInit;
isSpawned = true;
fig.UserData.isLeftClick = false;
end
end
%cntEffectの更新
cntEffect = max(cntEffect-1, 0);
else
if isSpawned
% 10フレームごとに2種類の絵を切り替える
if cntStep < 10
obj.CData = spriteSheetEnm(12:27,36:51,:);
else
obj.CData = spriteSheetEnm(12:27,53:68,:);
end
% cntStepの更新
cntStep = rem(cntStep+1, 10*2);
end
end
% RGBがそれぞれ116のpixを背景とみなして、そこだけ透明度を0にする
isBgArray = obj.CData(:,:,1) == 116 & obj.CData(:,:,2) == 116 & obj.CData(:,:,3) == 116;
obj.AlphaData = ~isBgArray;
pause(1/fps);
end
%%
function btnDownFnc(src, data)
switch(data.Source.SelectionType)
case 'normal' %左クリック
src.UserData.isLeftClick = true;
otherwise
src.UserData.isLeftClick = false;
end
end






