サッカーの守備フォーメーションらしきものを生成できるモデルを実装してみた

サッカーの守備フォーメーションらしきものを生成できるプログラムを書いたので紹介いたします．

# 成果物

以下のようなgifアニメーションが得られます．赤丸青点が選手の位置です．最終的に，4-4ブロックの4-4-2のような選手配置が得られています．
![voronoiFormationSample - コピー (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570966/b6be7f36-098f-429b-883a-bb6fbbb31dbd.gif)


# 概要
複数の点を空間にばらまき，点間や空間境界からの斥力を定義すると，それらの点は空間を均一に覆うように移動します．[「MATLABでvoronoi図のデータ形式を確認してみる」](https://qiita.com/konakalab/items/9f6d6dec55ca78c552f4)で示したのが具体例になります．

(前記事より再掲)
![testAnimated.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570966/072e9ba7-8099-8245-9f85-4498bdd9c2b5.gif)

サッカーの守備では相手にスペースを与えないことが重要であるので，基本的な配置は空間を均一に覆うように選ばれます．このことを，「選手間に働いている仮想的な斥力」で再現を試みます．各選手がピッチの端や他の選手から力を受けてその方向に移動すると仮定します．ある位置での力の合計が釣り合うと，選手はそこから動かなくなります．


この前提に加え，サッカーっぽさを出すために以下いろいろ仮定を入れました．

- ゴールキーパーの位置は固定
- 守備ラインの人数を指定し，その人数の縦方向の位置は同一とする
- 守備範囲を指定し，フォワードは「守備範囲より前にいる選手」と定義する
- ラインからは斥力，ゴールからは引力を発生させる．

## コード
mファイルで書いていましたが，[ライブスクリプトの Markdown 変換で楽して Qiita 投稿](https://qiita.com/eigs/items/513a17bd8cb2c5c5f435)が面白そうだったので早速利用してみました．出力を少し修正して利用しています．また，ちょっと冗長ですがコード全文を示します．

### 初期設定など

```matlab:makerFormation.mlx
clear;clc;close all

% ピッチサイズ
yard2meter=0.9144;
pitchsize=[68;105/2];
penaltyArea=[pitchsize(1)/2,0]+[-18 18;10 0]*yard2meter;

% 各ポジションの選手数
numOfPlayers=11 ;
numOfDFs=4;
numOfFWs=2;
defenseAreaDepth=35; % FW以外の選手はこの範囲内に配置する

% 斥力パラメータの設定．負は引力．
param.kTouchLine=1/4;
param.kGoalLine=1/2;
param.kGoal=-1/4;
param.kOppGoal=-1/4;
param.kDefenseArea=1/20;
param.kCenterLine=1/2;
param
```

### ピッチ描画

```matlab:makerFormation.mlx
figure
hold on;
axis equal
rectangle('Position', [0 0 pitchsize']);
rectangle('Position',  ...
    [pitchsize(1)/2-4*yard2meter,-3, 8*yard2meter 3]);
rectangle('Position',  ...
    [pitchsize(1)/2-22*yard2meter,0, 44*yard2meter 18*yard2meter]);
rectangle('Position',  ...
    [pitchsize(1)/2-10*yard2meter,0, 20*yard2meter 6*yard2meter]);
plot([0 pitchsize(1)],[defenseAreaDepth, defenseAreaDepth],'k:')
set(gca,'FontName','arial','FontSize',10)
```
### 選手位置の初期化
```matlab:makerFormation.mlx
pos=2*randn(numOfPlayers,2)+[pitchsize(1)/2, defenseAreaDepth/2];
[~,ind]=sort(pos(:,2));
pos=pos(ind,:);
pos(1,:)=[pitchsize(1)/2, 2];   %GK
pos(2:(1+numOfDFs),:)=[pitchsize(1)/2, defenseAreaDepth/2]+2*randn(numOfDFs,2); %守備ライン

% FWの初期配置を守備エリア(defenseAreaDepthで定義)よりも前にする
pos((numOfPlayers-numOfFWs+1):numOfPlayers,:)= ...
    2*randn(numOfFWs,2)+ ...
    [pitchsize(1)/2 defenseAreaDepth+(pitchsize(2)-defenseAreaDepth)/2];
posOld=inf(size(pos));
[~,ind]=sort(pos(:,2));
pos=pos(ind,:);
sctObj=scatter(pos(:,1),pos(:,2));
vrnObj=voronoi(pos(:,1),pos(:,2));
lineObj=plot([0 pitchsize(1)],[pos(2,2) pos(2,2)],'r:','LineWidth',1);
```

### ラインなどから定義されるポテンシャル場の描画

```matlab:makerFormation.mlx
figure(2)
[X,Y]=meshgrid(0:pitchsize(1), 0:pitchsize(2));
Z=zeros(size(X));
for n1=1:size(Z,1)
    for n2=1:size(Z,2)
        sumF=0;
            % タッチラインからのポテンシャル
            v=[-X(n1,n2),0];
            sumF=sumF+1/norm(v)*param.kTouchLine;
            v=[pitchsize(1)-X(n1,n2),0];
            sumF=sumF+1/norm(v)*param.kTouchLine;
            % ゴールラインからのポテンシャル
            v=[0, -Y(n1,n2)];
            sumF=sumF+1/norm(v)*param.kGoalLine;

            % ゴールからのポテンシャル
            v=[pitchsize(1)/2-X(n1,n2), -Y(n1,n2)];
            sumF=sumF+1/norm(v)*param.kGoal;

            %相手ゴールからのポテンシャル
            v=[pitchsize(1)/2-X(n1,n2), pitchsize(2)-Y(n1,n2)];
            sumF=sumF+1/norm(v)*param.kOppGoal;

            % (仮想的な)守備前線ラインからのポテンシャル
            v=[0, defenseAreaDepth-Y(n1,n2)];
            sumF=sumF+1/norm(v)*param.kDefenseArea;

            % センターラインからのポテンシャル
            v=[0, pitchsize(2)-Y(n1,n2)];
            sumF=sumF+1/norm(v)*param.kCenterLine;
            if sumF>0.25
                sumF=0.25;
            end
            Z(n1,n2)=sumF;
    end
end
colormap(cool)
contourf(X,Y,Z,'LineStyle','none');
hold on;
axis equal
rectangle('Position', [0 0 pitchsize']);
rectangle('Position',  ...
    [pitchsize(1)/2-4*yard2meter,-3, 8*yard2meter 3]);
rectangle('Position',  ...
    [pitchsize(1)/2-22*yard2meter,0, 44*yard2meter 18*yard2meter]);
rectangle('Position',  ...
    [pitchsize(1)/2-10*yard2meter,0, 20*yard2meter 6*yard2meter]);
plot([0 pitchsize(1)],[defenseAreaDepth, defenseAreaDepth],'k:')
set(gca,'FontName','arial','FontSize',10)
saveas(gca,'potentialField','png')
```

![potentialField.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570966/f93d5cba-b8aa-7955-fd84-c40d47238116.png)


各種ラインと仮想的な守備範囲ラインから斥力，ゴールからは引力を発生させるようなポテンシャルになっています．数式として書いた方が等高線や勾配もきれいに書けそうではありますが，今ところよくわかっていないので今後の課題とします．

### 選手位置の計算

フィールドと他の選手で定義されるポテンシャルの勾配の方向に動かします．
本当は停止条件が必要ですが，面倒なのでループの回数で止めています．

```matlab:makerFormation.mlx
figure(1)
for k=1:250
    for n1=1:size(pos,1)
        sumF=[0 0];
        % 他の選手からの斥力の和を計算する
        for n2=1:size(pos,1)
            if n1~=n2
                v=pos(n2,:)-pos(n1,:);
                sumF=sumF-v/norm(v)^2;
            end
            % タッチラインからの力
            v=[-pos(n1,1),0];
            sumF=sumF-v/norm(v)^2*param.kTouchLine;
            v=[pitchsize(1)-pos(n1,1),0];
            sumF=sumF-v/norm(v)^2*param.kTouchLine;
            % ゴールラインからの力
            v=[0, -pos(n1,2)];
            sumF=sumF-v/norm(v)^2*param.kGoalLine;
            
            % ゴールからの引力
            v=[pitchsize(1)/2-pos(n1,1), -pos(n1,2)];
            sumF=sumF-v/norm(v)^2*param.kGoal;
            
            %相手ゴールからの引力
            v=[pitchsize(1)/2-pos(n1,1), pitchsize(2)-pos(n1,2)];
            sumF=sumF-v/norm(v)^2*param.kOppGoal;
            
            % (仮想的な)守備前線ラインからの力
            v=[0, defenseAreaDepth-pos(n1,2)];
            sumF=sumF-v/norm(v)^2*param.kDefenseArea;
            
            % センターラインからの力
            v=[0, pitchsize(2)-pos(n1,2)];
            sumF=sumF-v/norm(v)^2*param.kCenterLine;
            
        end
        pos(n1,:)=pos(n1,:)+1*sumF;
        pos(1,:)=[pitchsize(1)/2, 2];   %GKの位置固定
        [~,ind]=sort(pos(:,2));
        pos=pos(ind,:);posOld=posOld(ind,:);
        tmpY=mean(pos(2:1+numOfDFs,2));
        pos(2:1+numOfDFs,2)=tmpY;
    end
    pos;
    
    % gifアニメ作成の設定
    title({[num2str(numOfDFs) '-' num2str(numOfPlayers-numOfDFs-numOfFWs-1) ...
        '-' num2str(numOfFWs) '. ' 'Iteration=' num2str(k)]})
    filename = 'voronoiFormationSample.gif'; % Specify the output file name
    [A,map] = rgb2ind(    frame2im( getframe(gcf)),256);
    if k == 1
        imwrite(A,map,filename,'gif','LoopCount',Inf,'DelayTime',1/2);
    else
        imwrite(A,map,filename,'gif','WriteMode','append','DelayTime',1/30);
    end
    
    % 数フレームに1回gifに追加する．
    if mod(k,2)==0
        k;
        sctObj.XData=pos(:,1);
        sctObj.YData=pos(:,2);
        [vx,vy]=voronoi(pos(:,1),pos(:,2));
        vrnObj(1).XData=pos(:,1)';
        vrnObj(1).YData=pos(:,2)';
        vx=[vx;nan(1,size(vx,2))];
        vy=[vy;nan(1,size(vy,2))];
        
        vrnObj(2).XData=reshape(vx,1, prod(size(vx)) );
        vrnObj(2).YData=reshape(vy,1, prod(size(vy)) );
        norm(posOld-pos)/norm(pos);
        lineObj.YData=[pos(2,2),pos(2,2)];
        if norm(posOld-pos)/norm(pos)<2e-6
            break
        end
        
%         pause(0.01)
    end
    posOld=pos;
    
end

imwrite(A,map,filename,'gif','WriteMode','append','DelayTime',1);
```

MATLABのテクニック的には，アニメーションを作成する際にはまずプロットのオブジェクトを保持しておき，その中のデータを書き換えた後にフレームを追加する部分を推しておきます(すでに有名な方法でしょうか…？)．単純なものは ` hold on`や` hold off`の切り替えでもなんとかなりますが，ボロノイ図のような複雑なものはデータを書き換える方が楽だと思います．

### 改めて成果物です．
![voronoiFormationSample - コピー (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570966/92d55b85-1199-4ec6-6923-fc7e01a32bb3.gif)

サッカー固有の事情(オフサイドライン，ゴールキーパー，ゴールの存在，など)を組み込むことで，実際に近い雰囲気の配置が得られました．

設定を変えるといろいろな形が出来上がります．

- 4-5-1と言いつつ5-4-1っぽい．
![fomration_20201217153739.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570966/55e43fc5-c29c-5376-0049-c52d164c5f96.png)
「指定した人数の縦方向の位置を一致させる」としか書いていないので，そのラインの直前に選手が配置されることもあります．

- 3-4-3
![fomration_20201217154241.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570966/9eca7f00-b748-3d7a-3f4f-09f6777516e9.png)
FWを横に広げる要因を定義していないので，中央に集まる形になります．今後の課題．

- プログラムの処理的には4-6-0と書いてありますが，4-3-3とか4-1-2-3と表記される形だと思います．
![fomration_20201217154807.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570966/86f8d2fc-5706-d50b-9857-2a920a1729a6.png)

他のスポーツでも似たようなことができると楽しそうですね．それでは！