search
LoginSignup
3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

GameMaker Studio Advent Calendar 2020 Day 22

posted at

updated at

【GameMaker Studio2】GML v2.3 の機能の紹介

GameMaker Studio v2.3 で実装された、GMLの機能について紹介します

配列の記述変更とチェインアクセサについて

v2.3以前では、多次元配列は以下のように記述していました

v2.3以前
array_2d[0, 0] = "hello";
array_2d[0, 1] = "world";

v2.3以降では以下の記述に変更されています

v2.3以降
array[0][0] = 1;
array[0][1] = "hello";
array[0][2] = 55.5;
array[1][0] = sprite_index;
array[1][1] = "world";
array[1][2] = -67.89;

これだけだとあまり必然性が感じられないのですが、次のチェインアクセサが実装されたことで扱いが便利になっています。

// ds_mapの中にある ds_list を取り出して、その中から ds_grip の値を取り出す
var _a = data[? "lists"][| 0][# 0, 0];

// ds_list から取り出した配列の値を変更する
data[| 0][@ 10] = 100;

// 配列 > ds_list > ds_grid > ds_map の値を変更する
data[0][| 10][# 3, 4][? "key"] = "hello world";

正直なところ、この例のようにデータ構造を深くネストすることはあまりないかもしれませんが、いざ階層的なデータ構造を作ろうとしたときに、この記述方法ができるのは便利なのではないかと思います

function キーワード (関数の動的定義)

v2.3での変更で、個人的にはこれが一番大きなものと考えています。
今までは関数を使うためにはスクリプトを作成する必要があったのですが、このキーワードが追加されたことで、オブジェクト内に関数を直接定義できるようになりました。

この威力を知るために、サンプルコードを書いていきます。
stg.gif

例えばこんな感じのシンプルなシューティングの自機を作ってみます。
プレイヤー (obj_Player)を作成し、Createイベントには以下のように記述します。

obj_PlayerのCreateイベント
// ショットのインターバル用の値
timer = 0;

そしてStepイベントは以下のように記述します。

obj_PlayerのStepイベント
// 移動処理
var vx = keyboard_check(vk_left) ? -1 : (keyboard_check(vk_right) ? 1 : 0);
var vy = keyboard_check(vk_up)   ? -1 : (keyboard_check(vk_down)  ? 1 : 0);
if(vx != 0 or vy != 0) {
  // 移動実行
  var dir = point_direction(0, 0, vx, vy); // 移動角度を計算
  var spd = 8; // 移動速度
  x += lengthdir_x(spd, dir);
  y += lengthdir_y(spd, dir);
}

// ショット判定.
if(keyboard_check(vk_space)) {
  // SPACEキーでショットを発射
  if(timer%4 == 0) {
    // 4F間隔でショットを撃つ
    var _inst = instance_create_depth(x, y, 0, obj_Shot);
    _inst.speed = 20; // 速度20
    _inst.direction = 90; // 90度の方向
  }
  timer++;
}
else {
  // ショットを撃たないときはインターバル値をリセット
  timer = 0;
}

ここで注目するのはショットを撃つ(obj_Shotを生成する) 処理です。

var _inst = instance_create_depth(x, y, 0, obj_Shot);
_inst.speed = 20; // 速度20
_inst.direction = 90; // 90度の方向

現状では、1箇所でしかこの記述をしていないので問題とはならないですが、もしこのショットを撃つ処理を複数の場所で行う場合には、このコードを流用することになります。
その場合にコードをコピーするよりも関数にまとめれば1つだけ記述すればよくなるのですが、v2.3以前では、そのためだけにスクリプトを作成する必要がありました。

v2.3では、functionキーワードを使うと、処理を簡単に使い回せるようになります。
obj_PlayerのCreateイベントを以下のように変更します。

obj_PlayerのCreateイベント
// ショットのインターバル用の値
timer = 0;

// ショット処理を定義
shot = function(dir, spd) {
  var _inst = instance_create_depth(x, y, 0, obj_Shot);
  _inst.speed = spd;
  _inst.direction = dir;
}

新たにショット処理を定義しました。
shotは引数、dirとspdを受け取って、その値でショットを発射する関数となります。

このように定義すると、obj_Player は以下のように記述できます。

obj_PlayerのStepイベント
// 移動処理
var vx = keyboard_check(vk_left) ? -1 : (keyboard_check(vk_right) ? 1 : 0);
var vy = keyboard_check(vk_up)   ? -1 : (keyboard_check(vk_down)  ? 1 : 0);
if(vx != 0 or vy != 0) {
  // 移動実行
  var dir = point_direction(0, 0, vx, vy); // 移動角度を計算
  var spd = 8; // 移動速度
  x += lengthdir_x(spd, dir);
  y += lengthdir_y(spd, dir);
}

// ショット判定.
if(keyboard_check(vk_space)) {
  // SPACEキーでショットを発射
  if(timer%4 == 0) {
    // 4F間隔でショットを撃つ
    // 10度間隔の3WAY
    shot(90, 20); // 正面
    shot(90-10, 20); // 右側(-10)
    shot(90+10, 20); // 左側(+10)
  }
  timer++;
}
else {
  // ショットを撃たないときはインターバル値をリセット
  timer = 0;
}

3WAYに発射できるようにしてみました。このくらいの処理であれば for文でも実装できますが、サンプルとしてはこのような実装イメージです。
3way.gif

プレイヤーが1種類だけであれば、function を使うまでもないかもしれませんが、複数の自機の種類があってそれぞれ異なるショットを実装する場合にはショットを撃つ部分が共通されていると便利です。
また、敵オブジェクトが敵弾を撃つ場合は、基本となる敵オブジェクトを継承して派生していくので、このような便利関数を親オブジェクトに実装しておけば、使い回しやすいと思います。

ということで、このサンプルプロジェクトは以下からダウンロードできます。

function を使うときの注意点ですが、function() 外にある変数を扱う場合は注意が必要です。例えば以下のコードは正常に動作しません。

// ----------------------------
// Createイベントで以下の記述をする
var hoge = "hogehoge";

print = function() {
  show_debug_message(hoge); 
};


// ----------------------------
// Stepイベントで以下の記述をする
print();

function() の外で、変数 "hoge" が定義されていますが、"var" キーワードがついているため、この変数は function内で扱うことができません。
そして Stepイベントで呼び出すと未定義の変数にアクセスするため、エラーで停止が発生します。
Created_with_GameMaker_Studio_2_と_Created_with_GameMaker_Studio_2.png

ローカル変数はうまくいかないですが、インスタンス変数にアクセスする場合は問題なく動作します。例えばCreateイベントを以下のよう記述します。

Createイベント
print = function() {
  show_debug_message(string(x) + "," + string(y));  
};

"x" と "y" はそのオブジェクトのインスタンスが存在する限り存在するため、このように記述しても問題なく動作します。

構造体

データ構造のみ

ds_mapのような名前のあるデータ構造を、以下の記述で定義できるようになりました。

// ステータス
status = {
  name: "勇者", // 名前
  hp: 100,     // HP
  mp: 20,      // MP
  atk: 15      // 攻撃力
};

show_debug_message("名前: " + status.name);
show_debug_message("HP: " + string(status.hp));
show_debug_message("MP: " + string(status.mp));
show_debug_message("攻撃力: " + string(status.atk));

出力結果

名前: 勇者
HP: 100
MP: 20
攻撃力: 15

このようにキャラクターのステータスデータなどを簡単に記述できます。JSONのような記法となっており、他の変更含めてJavaScriptに寄せていっているなぁ……というのが個人的な印象です。

constructor キーワードでオブジェクト化して staticキーワードで関数をもたせる

次にステータスの出力処理を使い回せるようにします。

function StatusObj() constructor {
  name = "勇者";  // 名前
  hp = 100;      // HP
  mp = 20;       // MP
  atk = 15;      // 攻撃力

  // 情報を出力する  
  static print = function() {
    show_debug_message("名前: " + name);
    show_debug_message("HP: " + string(hp));
    show_debug_message("MP: " + string(mp));
    show_debug_message("攻撃力: " + string(atk));
  }
}

// 初期ステータスで出力
status = new StatusObj();
status.print();

show_debug_message("------------------------");
// ステータスを書き換えて出力
status.name = "魔法使い";
status.hp = 50;
status.mp = 120;
status.atk = 5;
status.print();

実行すると以下のように出力されます。

名前: 勇者
HP: 100
MP: 20
攻撃力: 15
------------------------
名前: 魔法使い
HP: 50
MP: 120
攻撃力: 5

print() 関数をオブジェクトに持たせるようにして、情報の出力を簡単に使い回せるようにしました。

さらに contructorキーワードに引数を指定することで、初期パラメータを外部から渡せるようになります。

function StatusObj(_name, _hp, _mp, _atk) constructor {
  name = _name;  // 名前
  hp   = _hp;    // HP
  mp   = _mp;    // MP
  atk  = _atk;   // 攻撃力

  // 情報を出力する  
  static print = function() {
    show_debug_message("名前: " + name);
    show_debug_message("HP: " + string(hp));
    show_debug_message("MP: " + string(mp));
    show_debug_message("攻撃力: " + string(atk));
  }
}

// 生成時に初期パラメータを渡す
status = new StatusObj("賢者", 80, 100, 10);
status.print();

実行結果

名前: 賢者
HP: 80
MP: 100
攻撃力: 10

このようにデータ構造と関数がセットになっていると、処理の使い回しがとても楽になります。

例えば、自作の脱出ゲームでは、テキスト処理が「会話テキスト」「選択肢テキスト」「通知メッセージ」「バックログ」と様々な場面で似たような処理が行われるので、 contructor を使用して処理を共通化させました。

adv1.gifadv2.gif

その他

v2.3のその他の機能として、例外処理(try〜catch)などがありますが、個人的に使う場面はあまりなさそうなので省略します。

おそらくネットワーク対戦ゲームなどでは不測のエラーが発生しやすいので、そういった機能を実装する場合のエラー処理として使うと良さそうです。
try〜catchについては、以下のページ(英語)に細かい説明があるので、気になる場合は参考にしてみると良いかもしれません

追記

struct の 「$」キーワードによるアクセス

v2.3.1 のバージョンアップでstructのデータにアクセスする際に、「$」キーワードを使ったアクセスができるようになりました。
例えばこのコードを実行すると、

var hero = {
  name: "勇者",
  hp: 100,
  mp: 20,
};

show_debug_message("name: " + hero[$ "name"]);
show_debug_message("hp: "   + string(hero[$ "hp"]));
show_debug_message("mp: "   + string(hero[$ "mp"]));

以下のように出力されます。

name: 勇者
hp: 100
mp: 20

この例だと記述量が増えて面倒になっただけに見えるかもしれませんが、データ名が文字列の組み合わせで決まるような場合などにこの方法が役に立つことがありそうです。
ちなみに、variable_struct_get() / variable_struct_set() を使っても同じようなことができるので、それらの関数のシンタックスシュガー(簡易記述文法)となります。

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
What you can do with signing up
3
Help us understand the problem. What are the problem?