Gamemaker:studio2作品をアツマールに投稿する。【アツマールAPI】【SPINE】　

Gamemaker:studioの言語GMLはシンプルで個人開発に向いたゲーム開発ツールなのですが、アツマールに投稿している作品をあまり見かけません。

今回、アツマール主催のクリエーターズキャンプ２ndに参加してGMを使用してhtml5の作品を制作しましたのでアドベントカレンダー記事の健忘録として残します。

キャンプの運営様、サポーター様、参加者の皆様にお礼を申し上げます。

●●●●●●●●●●●●GM製のゲーム
ゲームメーカー？なにそれ食べれるの？という人向け
作られたゲームリストです。知っているゲームがあると思います。

Undertail
downwell
Hyper Light Drifter
VA-11 hall-A
DEATH GUNBIT
HOT LINE MIAMI
Momodora
risk of rain
spelunky
abyss crawlers plus
.....


●●●●●●●●●●●●アツマールでのGAMEMAKERのメリット、デメリット
メリット
ロゴやマーク無しで起動できる。
素材が入っていないので最小限から作ることができる。
GMでスプライトアニメーションを描くことも出来る。（ドット絵Asepriteがオススメ）
スクリプトが難しい人向けにブロックを組み合わせてゲームを制作するモードが用意されている。
GMLスクリプトもC#等に比べると敷居が低い。（セミコロン忘れても大丈夫！！）
GIFアニメをインポートできてで出力の時に、不要なエリアをカットして複数の画像をまとめてパックしてくれるので60FPSでも比較的にぬるぬる動く。
アクションゲームが比較的に簡単に制作できます。

デメリット
ツクールのように最初から素材が用意されていない。
フォントが組込みできないので大きい文字での長文は苦手
スマホなどでテクスチャーが真っ黒になるゲームがある。
EXE版と違いhtml5吐出しにはバグが多い。(2019/12減った）
３D表現もできるが、リアルなのは難しそう。
マーケットプレイスにあるアセットでポストエフェクト（ブルームなど）関係は少ないかも・・

●●●●●●●●●●●●画面の見方。
![screen.png](https://qiita-image-store.s3.amazonaws.com/0/362013/d324aca3-5310-8530-f3b8-5570fe45514f.png)

基本右側のリソースで管理する感じで、スプライトやサウンドをオブジェクトに組み込んでルームに配置します。
オブジェクト内にGMLというスクリプトを書いて動かします。

スクリーンサイズはアツマールで表示できるサイズがありますので横長の場合は816X624が基本のようですが、GMで出力した場合プレイ環境によっては右端が隠れるみたいなのでちょっと750とかで探ってみてください。


●●●●●●●●●●●●Fontを画像化する。
html5にそのまま吐き出すとフォントが無いので文字が表示されません。
そのためアトラス化する必要があります。
![mojimokj.png](https://qiita-image-store.s3.amazonaws.com/0/362013/2ca5ee85-021c-0354-aaed-0a3e85305aea.png)
フォントと文字を選んでadd→FromCodeをすることで作中で使っている文字を自動で取出してくれるのでadd fontで登録します


●●●●●●●●●●●●ブラウザで確認する。
![html5.png](https://qiita-image-store.s3.amazonaws.com/0/362013/e2f2b157-379a-15e5-ae07-cf6f2354e9ec.png)
プラットフォームをhtml5でブラウザ指定して再生するとチェックできるがたまに真っ白になる時があります。


●●●●●●●●●●●●html5を吐き出す前に設定する
![export.png](https://qiita-image-store.s3.amazonaws.com/0/362013/6ca06a62-9c34-03f4-c804-999112d9373f.png)
ビルドフォントは不要なファイル.yyを吐き出してアツマールで読み込みエラーになるのでチェックを外しとく。

![名称未設定-13.png](https://qiita-image-store.s3.amazonaws.com/0/362013/20bae530-a0d5-a24a-95b7-09833e81e420.png)
デプロイするとzipが吐き出されますがiniファイルはいらないので削除して再zip化してアツマールに登録します。

■Spineを使用している場合はそのままアツマールに登録できないので■ ■ ■ ■ ■ ■
ほにゃらら.skelの拡張子をほにゃらら.gifに書き換えて
ほにゃらら.jsからCTRL+Fでほにゃらら.skelを検索しほにゃらら.gifに置き換えることで可能になります。

参考作品　DarkCityの悪魔（Spine＆Aura2使用）
https://game.nicovideo.jp/atsumaru/games/gm7969


●●●●●●●●●●●●GMとアツマールAPIとの連携
■セーブと保存  ■ ■ ■ ■ ■ ■
アツマールのAPIに保存とロードが用意されていますが、以下のアセットが手軽です。
https://marketplace.yoyogames.com/assets/5823/html5-web-storage

■アツマールのコメントをシーン毎に変更する

```
function AtsumaruComment(scenename) {
    window.RPGAtsumaru.comment.resetAndChangeScene(scenename);
}
```


■アツマールからURLを開く  ■ ■ ■ ■ ■ ■
参考作品　JUKEBOXmini
https://game.nicovideo.jp/atsumaru/games/gm7777


まずatsumaruAPI.jsに以下の内容を書いて保存します。

```
function openAtsumaruURL(str) { 
    window.RPGAtsumaru.popups.openLink(str)
}
```

GMで呼び出すにはエクステンションにJSを登録する必要があります。
リソースのエクステンションのところでcreate Extension (ALT+E)します。
![createExtension.png](https://qiita-image-store.s3.amazonaws.com/0/362013/e4e66827-404c-6a28-e3d9-1b1a63308c39.png)
Resourceのファイルに先程作ったatsumaruAPI.jsを追加し右に伸びたパネルからFunctionも追加します。
![createfunction.png](https://qiita-image-store.s3.amazonaws.com/0/362013/cf80cf43-b292-736c-6c70-106543ecb912.png)

JSのfunctionを登録します。
![jscopypaste.png](https://qiita-image-store.s3.amazonaws.com/0/362013/c8185df1-13b8-bd64-387a-93a17aa08d9d.png)

あとはGamemakerでobjectを押したらジャンプするようにします。
![openurl.png](https://qiita-image-store.s3.amazonaws.com/0/362013/39a2ddd8-4fc5-98c2-e6b7-55ff90682236.png)


```
   if mouse_check_button(mb_left)
   {
   if position_meeting(mouse_x, mouse_y, o_jk) {
		openAtsumaruURL("https://game.nicovideo.jp/atsumaru/games/gm7711")
	   }
   }
```


■スコアボードとのやりとり■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
参考作品
　・囲みっくす
https://game.nicovideo.jp/atsumaru/games/gm7670
　・JKリフレ♥はじめました。
https://game.nicovideo.jp/atsumaru/games/gm7793



URL開くと同じようにエクステンションで登録して作ります。スコアボードに0番は無いので注意

```
//GML
openAtsumaruRanking(1);　　//一番目のスコアボード表示
saveAtsumaruRanking(1, 1234)　　//一番目のスコアボードに1234点を保存
```

JSは以下

```
function openAtsumaruRanking(num) { window.RPGAtsumaru.experimental.scoreboards.display(parseInt(num)) };
function saveAtsumaruRanking(num0, num1) { window.RPGAtsumaru.experimental.scoreboards.setRecord(parseInt(num0, 10), parseInt(num1, 10)) };
```

気をつける部分は、同じフレーム内でスコアを保存して、スコアボード開いてもまだ最新のスコアに更新されていないのでアラーム等でスコアボードを開くのを遅らせると良いです。

■Gamemakerでサーバのユーザーデータ（JSON）を文字列に変更してGamemakerに取り込みデコードして取り出す。■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
0.ユーザー情報の取得を可能にする（ゲームの起動時のINIT段階でやっておく）
1，Gamemakerからエクステンション→JavaScript→アツマールAPIを呼び出す
2，アラーム等の時間差で取得する。
アツマールのオブジェクトをそのままエクステンション（整数か文字列のみ）で送受出来ないので一度JsでJsonStringfyで文字列に変更してGamemakerに戻す。
//java script

```
//ユーザーデータの閲覧を有効にする（ゲーム中1回）
function AtsumaruEnable() {
    RPGAtsumaru.experimental.interplayer.enable();//ユーザー情報取得を有効化
}
```

```
function AtsumaruCallUserData() {//遅延があるので呼び出しだけ
       window.RPGAtsumaru.experimental.user.getSelfInformation().then(function (result) {
            window._result = result;//
    });
}
```

```
function AtsumaruGetUserData() { //alarm等で遅らせ文字変換後回収
    var data = JSON.stringify(window._result);
    return data;
}
```

```
function AtsumaruShowMessage(moji) {//デバッグ用コンソールでメッセージ出力。
    console.log(moji);　//GMLのshow_debug_message()と同じ
}
```

Gamemaker側でデコードJson_decode()してDSMAPに格納してds_map_find_value(mapp, "name")でユーザー名前を取得しました。

```
//gamemaker側
var mapp = json_decode(AtsumaruGetUserData());
moji = ds_map_find_value(mapp, "name");
```




■gamemakerで最新のプレイヤーのJSONデータ（最大1000件のid,name）を取得する。■ ■ ■ ■ ■ ■
Javascriptは、基本上のJSONと同じやり方と同じく最初にユーザー有効にして吸い上げる。

```
// javascript側
function AtsumaruCallRecentData() {
    window.RPGAtsumaru.experimental.user.getRecentUsers().then(function (result) {
        window._result = result;
        console.log(result);
    });
}

function AtsumaruGetRecentData() {
    var data = JSON.stringify(window._result);
    console.log(data);
    return data;
}
```

複数データがあるのでリストを作成してその中から抽出する。

```
//gamemaker側
var resultMap = json_decode(AtsumaruGetRecentData());
var list = ds_map_find_value(resultMap, "default");
var size = ds_list_size(list);
for(var n = 0; n <ds_list_size(list); n ++;)
   {
   var map = ds_list_find_value(list,n);
   var curr = ds_map_find_first(map);
   while(is_string(curr))
      {
		global.ids [n] = ds_map_find_value(map, "id");
		global.Name [n] = ds_map_find_value(map, "name");

		curr = ds_map_find_next(map,curr);
      }
   ds_map_destroy(map);
   }
ds_list_destroy(list);
ds_map_destroy(resultMap);
```

■SAVE：アツマールサーバにJsonで保存する。■ ■ ■ ■ ■ ■

```
//GamemakerでJsonデータを作ってAPIに送る
//Save test;
var _map=ds_map_create();
ds_map_add(_map,"buki","sword");
ds_map_add(_map,"gold",3456);
ds_map_add(_map,"mhp",100)
var _string = json_encode(_map)
AtsumaruSetItems(_string);
```

```
//javascript
//Save ItemData
function AtsumaruSetItems(data) {
    window.RPGAtsumaru.storage.setItems([
        { key: "json", value: data }
    ]);
}
```

LOAD：アツマールサーバからJsonデータをロードする。



```
//javaScript
//LOAD ItemData
function AtsumaruCallItems() {
    window.RPGAtsumaru.storage.getItems().then(function (result) {
        var file = result.find(function (obj) { return obj.key === 'json' });
        window._result = file.value;

    });
}

function AtsumaruGetItems() {
    var data = window._result;
    return data;
}
```

Gamemaker側 

```
//呼び出し
AtsumaruCallItems();
```

```
//時間差で取得
var items=json_decode(AtsumaruGetItems());
moji = ds_map_find_value(items, "buki");
AtsumaruShowMessage(moji)
ds_map_destroy(items);
```

■DS_LISTをJSONで記録と読み取り実験メモ■ ■ ■ ■ ■ ■

上で作った関数でキーのJsonを選択できるように変更してそのまま使います。
java script側

```
//保存する。
function AtsumaruSetItems(json,data) {
    window.RPGAtsumaru.storage.setItems([
        { key: json , value: data }
    ]);
    console.log(data);
}


//LOAD ItemData
function AtsumaruCallItems(json) {//呼び出し
    window.RPGAtsumaru.storage.getItems().then(function (result) {
        var file = result.find(function (obj) { return obj.key === json });
        window._result_item = file.value;
    });
    console.log("Call Item");
}

function AtsumaruGetItems() {//取得
    var data = window._result_item;
    return data;
}
```
GameMaker側
SAVE　プレイヤー情報infoとパーティ情報partyの2つを格納します。

```
//Save test;

var _map=ds_map_create();

var info=ds_list_create();
var party=ds_list_create();

ds_list_add(info,["yaneGKingdom","9odslayer","sofia",80,234,999,123,444])//cran,nickname,lover,gold,water,food,kill,stage

ds_list_add(party,["taro","soldier",3,10]);//party1(name,work,Lv,hp)
ds_list_add(party,["hanako","mage",50,100]);//party2
ds_list_add(party,["mayumi","priest",5500,5000]);//party3
ds_list_add(party,["kaneda","king",9999,9999]);//party4

ds_map_add_list(_map, "info", info);
ds_map_add_list(_map, "party",party);
_string = json_encode(_map);

AtsumaruSetItems("json",_string);

ds_map_destroy(_map);
ds_list_destroy(party);
ds_list_destroy(info);


```

LOAD　時間差が必要なので先にCALLします。

```
//呼び出し
AtsumaruCallItems("json");
```

そしてゲッツ

```
//習得してデコード
var resultMap=json_decode(AtsumaruGetItems());
//party情報
var list = ds_map_find_value(resultMap, "party");
var size = ds_list_size(list);
show_debug_message(size)
var data=list[| 1];
AtsumaruShowMessage(data[| 0])
AtsumaruShowMessage(data[| 1])
//item情報
var list = ds_map_find_value(resultMap, "info");
var size = ds_list_size(list);
data=list[| 0];
AtsumaruShowMessage(data[| 0])
AtsumaruShowMessage(data[| 1])

//DS破棄
ds_list_destroy(list);
ds_map_destroy(resultMap);
```
encord前とDecord後で書式が違うので注意

■課金APIを外した最新版AtsumaruAPI.js。

```
//ユーザーデータ参照を有効にする
function AtsumaruEnable() {
    RPGAtsumaru.experimental.interplayer.enable();

}



//プレイヤー情報を取得する。呼び出し。
function AtsumaruCallUserData() {
        window.RPGAtsumaru.experimental.user.getSelfInformation().then(function (result) {
            window._result_info = result;
    });
}

function AtsumaruGetUserData() {//遅延させて取得
    var data = JSON.stringify(window._result_info);
    return data;
}



// 最近のプレイヤーのデータを呼び出す。
function AtsumaruCallRecentData() {
    window.RPGAtsumaru.experimental.user.getRecentUsers().then(function (result) {
        window._result_data = result;
        });
}
function AtsumaruGetRecentData() {//遅延させて取得
    var data = JSON.stringify(window._result_data);
    
    return data;
}

//Save ItemData
function AtsumaruSetItems(json,data) {
    window.RPGAtsumaru.storage.setItems([
        { key: json , value: data }
    ]);
    console.log(data);
}


//LOAD ItemData
function AtsumaruCallItems(json) {
    window.RPGAtsumaru.storage.getItems().then(function (result) {
        var file = result.find(function (obj) { return obj.key === json });
        window._result_item = file.value;

    });
    console.log("Call Item");
 

}

function AtsumaruGetItems() {
    var data = window._result_item;
    return data;
}





//URLページを表示する
function AtsumaruOpenURL(str) {
    window.RPGAtsumaru.popups.openLink(str);
}

//Debug コンソール出力//show_debug
function AtsumaruShowMessage(moji) {
    console.log(moji);
}
```

GM側でログインチェック

```
//create
userid=0;
username="";
AtsumaruEnable() 
AtsumaruCallUserData() 
AtsumaruComment("initscene") 
alarm[1]=room_speed*2;

//alarm[1]
if is_undefined(AtsumaruGetUserData()){room_goto_next()}//次のルームでログイン案内を表示する。
var userMap=json_decode(AtsumaruGetUserData())
userid = ds_map_find_value(userMap, "id");
username = ds_map_find_value(userMap, "name");
ds_map_destroy(userMap);


//drawGUI
draw_set_font(f_japan)
draw_set_color(c_white)
draw_text(10,10,"userid:"+string(userid)+" username"+username);
```



■Html5での問題
GamemakerのHtml5エクスポーターにはまだバグが数多くあるのでHtml5環境で確認しながらすると良いですが、エラーがあってもそのまま動く事があるので、たまにWindowsビルドしてエラーチェックすると良いです。
ダークシティの悪魔製作時にまだいくつかSpineバグかありましたが、Spineの最新3.7に対応できるようにGameMakerで修正されたようです。

■モバイルでプレイ時の問題
PCは問題ないのですがiPadでスプライトが真っ黒だったので、引き続き解決方法を模索します。

■動画を再生させたい場合。
下記のアセットを使用してアツマールでMpegやWebmが再生できたのを確認しました。

【exVideo HTML5】動画はブラウザのみです。

■おすすめダイアログ
4_devsの作ったBase_VN
https://marketplace.yoyogames.com/assets/6579/base_vn
小規模なゲームにこの選択肢によるジャンプ出来るVNエンジンは、非常に使い勝手が良い。

特徴：
選択肢による分岐があり、マウスとキーボードに対応している。
各オブジェクトでシナリオを管理する
プレイ中にRoomにツリー上に配置したオブジェクトにシナリオジャンプ出来るのでデバッグが容易である。

苦手
文章毎にこまかくサウンドを変えたりは出来ない。

使い方。
・選択肢による分岐が無い→o_sceneを複製して使う
・選択肢による分岐がある→o_scene_optionsを複製して使う
1行目のcreate のevent_name="scene1"とするとroomに配置した時に一緒にo_controlと置くだけで最初に開始するobjを認識する。
最終行のnext_event="scene2"とすると次のシナリオscene2のオブジェクトに自動で跳ぶ;
o_textboxのDrawEndでメッセージボックスの背景縁取りとテキスト色の設定。
o_controlのcreateでメッセージボックスの色と透明度調整

基本全身中央揃えなので、バストアップの場合、スプライト画像を半分の解像度にしてボトムセンターにしてo_textboxのDrawEndのスプライト表示項目を下のように書き替えることで中央下揃え2倍になる。

```
if drawing1!=0{
	draw_sprite(drawing0,0,room_width*0.2 - global.face_xoffset, room_height - global.face_yoffset)
}

↓↓↓

if drawing1!=0{
	draw_sprite_ext(drawing1,0,room_width*0.5 - global.face_xoffset, room_height,2,2,0,c_white,1)
}
```

◆RTSのように画面をドラッグでスクロールする場合Gamemakerにはジェスターというイベントがあるのですが、html5では使えないようです。
そのためドラッグ中は1フレームの前のマウスのポジションと比較してカメラのポジションを設定する事で出来た。

```
//create event
camera = view_camera[0];
drag_now=false;
bk_x=0;
bk_y=0;
camera_set_view_pos(camera,o_start_pos.x-view_get_wport(0)/2,o_start_pos.y-view_get_hport(0)/2);
//
```

```
//step Event
if mouse_check_button_pressed(mb_left)
   {
	drag_now=true;
	bk_x=mouse_x;
	bk_y=mouse_y;
   }
   
if mouse_check_button_released(mb_left)
   {
	drag_now=false;
   }   
   

if(drag_now){
var _x=bk_x-mouse_x+camera_get_view_x(view_camera[0]);
var _y=bk_y-mouse_y+camera_get_view_y(view_camera[0]);


_x=clamp(_x,0,2000)
_y=clamp(_y,0,2000)
camera_set_view_pos(camera,_x,_y);

}

```


■おわりに
これから、GM使ってアツマールに投稿してみようと思う人が現れたら嬉しいです。

それとアツマールのGM投稿初作品の「JKパンチ❤」です。
https://game.nicovideo.jp/atsumaru/games/gm7711
自作ゲームフェス2019で果敢賞をいただきました。

ありがとうございました。

yaneG