2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【MATLAB】ドット絵を切り替えてキャラクターのモーション作成 ~歩行/攻撃/ダメージエフェクト~

2
Posted at

概要

MATLABでドット絵のシーケンスを組んで、モーションのようなものを作成してみました。
Image関数を使って画像をグラフィックオブジェクトとしてaxes上に描画し、そのCDataを適宜切り替えてモーションのように見せています。また、figureのコールバック関数を使用してマウスクリックを判定し、クリック時には攻撃モーションのようなアニメーションを表示します。最後には、カラーパレットをフレームごとに切り替えてダメージエフェクトのようなものも作ってみました。

Video Project 2_Damaged.gif

Video Project 3.gif

参考リンク

プログラム構成

今回の記事を書くにあたり、下記記事を参考にさせていただきました。isgraphicsを使用した無限ループだったり、コールバック関数の作成について大変参考になりました。ありがとうございました。

画像元

使用する画像はこちらのサイトのものをお借りしました。ゼルダ作品である理由は私が大好きだからです。いろいろな素材が1枚の画像にまとめられており、非常に使いやすかったです。海外のファンメイドコンテンツってすごい…。

Playable Characters - Link.png

歩きモーション

今回は簡単化のために、正面方向のみを考えます。
原理としては、一定時間ごとに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を使って透明度をわちゃわちゃしてる部分については、こちらの記事で詳細を解説しています。よろしければご覧ください。

実行結果

Video Project 2.gif

攻撃モーション

攻撃モーションも考え方は同様です。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
実行結果

Video Project 2_Attack.gif

ダメージエフェクト

せっかくなのでダメージエフェクトまでやってしまいましょう。メインループの最後に、ダメージ中(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
実行結果

Video Project 2_Damaged.gif

余談:敵キャラでも

クリックごとに、出現⇔消滅を繰り返すオクタロックです。

使用した画像

敵キャラ

Enemies & Bosses - Overworld Enemies.png

消滅エフェクト

Miscellaneous - Enemy Death.png

出現エフェクトは、リンクの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
実行結果

Video Project 3.gif

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?