RPGツクールMZのゲームでChatGPT APIを利用する（2023/03/10追記）


RPGツクールMZのゲーム内で、ChatGPT APIを利用する簡易的な方法の覚え書きです。

https://twitter.com/rahaandsary/status/1631437044126978048

とりあえず、という事で。

# 基本編

必要なもの
(1) PHPが動作するサーバ（筆者がPHP専なので使っていますが、出来る人はpythonでもrubyでも）
(2) ChatGPT APIのAPIキー
(3) ツクールMZ本体

まずPHPが動作するサーバ上にchatgpt_api.phpを設置します。
【ChatGPTのAPIキー】と、systemロールのcontent内を適宜書き換えてください。

```php:chatgpt_api.php
<?php

// ツクールMZからのメッセージをPOSTで受け取る
$message = htmlspecialchars($_POST["message"],ENT_QUOTES);

// ChatGPT APIへのリクエスト構築
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.openai.com/v1/chat/completions");
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Authorization: Bearer 【ChatGPTのAPIキー】'));

$data = array('model' => 'gpt-3.5-turbo');
$data["messages"] = array();
$data["messages"][] = array('role' => 'user', 'content' => $message);
// systemロールでChatGPTに役割を演じさせることが可能
$data["messages"][] = array('role' => 'system', 'content' => "貴方はロボットです。語尾に必ず「デス」をつけてください。");

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$result = curl_exec($ch);
$response = json_decode($result, true);
curl_close($ch);

// ツクールMZには受け取ったテキストを自動改行する機能が無いので、
// 30文字で強制的に改行コードを入れている
$res = mb_wordwrap($response["choices"][0]["message"]["content"], 30, '\n', true);
print $res;

// マルチバイト対応の改行関数
function mb_wordwrap($string, $width=75, $break="\n", $cut = false) {
    if (!$cut) {
        $regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){'.$width.',}\b#U';
    } else {
        $regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){'.$width.'}#';
    }
    $string_length = mb_strlen($string,'UTF-8');
    $cut_length = ceil($string_length / $width);
    $i = 1;
    $return = '';
    while ($i < $cut_length) {
        preg_match($regexp, $string, $matches);
        $new_string = (!$matches ? $string : $matches[0]);
        $return .= $new_string.$break;
        $string = substr($string, strlen($new_string));
        $i++;
    }
    return $return.$string;
}

?>
```

会話は記憶しませんので、2回目の質問で1回目の質問内容を参照すること（しりとりetc）は出来ません。
assistantロールを使うことでその実装も可能ですが、ここでは割愛します。

ツクールMZのプロジェクトを起ち上げ、イベントを作成します。
デモでは［名前入力の処理］を使って、テキスト入力ウィンドウを出しています。

アクターID:9に入れた名前を変数1に格納。変数1の値をchatgpt_api.phpにPOSTします。
※デモでは利便性の問題から一旦変数1に入れていますが、そのまま$gameActors.actor(9).name()をPOSTしても構いません。
![FqQHctoaAAAXE1H.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/9f4dfa4c-ab48-a4a9-c4c3-ad72b4eff263.jpeg)

ツクールMZには、変数に質問を代入する処理の直後に、スクリプトで次の様に書いてください。

```javascript:ツクールMZ
const url = "chatgpt_api.phpのパス";
const variableId = 1;
const variableValue = $gameVariables.value(variableId);
fetch(url, {
  method: "POST",
  headers: {"Content-Type": "application/x-www-form-urlencoded"},
  body: `message=${encodeURIComponent(variableValue)}`
})
.then((response) => response.text())
.then((data) => {
  const message = data.replace(/\\n/g, '\n');
  $gameMessage.add(message);
})
.catch((error) => console.error(error));
```

これでChatGPT APIに接続するロボットの完成です。

# ローディング処理の追加 （2023/03/10追記）

ChatGPT APIとの通信には早くても2〜3秒、長くて5秒以上の待ち時間が発生します。上のコードではレスポンスを待つ間に動き回れてしまいます。
そこで、待ち時間の間にローディング画像を挟み、動き回れない様にしてみました。（してみました、というか実はChatGPTにコードを改修してもらいました）

まずは、下の様な画像を作って「Loading2.png」としてsystemフォルダに入れてください。
![Loading.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/e37f3d78-f268-6c21-a43f-7d9c1b02b30d.png)
スクリプトを次の様に書き換えてください。
少し長いですが、現行のツクールMZは長いスクリプトを入れられるので大丈夫。

```javascript:ツクールMZ
const url = "chatgpt_api.phpのパス";
const variableId = 1;
const variableValue = $gameVariables.value(variableId);

// キー入力やタッチ操作を無効化し、画面を更新しないようにする
Input.clear();
TouchInput.clear();
SceneManager._scene._waitCount = 1;

// ロード画像を表示
const loader = new Sprite();
loader.bitmap = ImageManager.loadSystem("Loading2");
loader.x = 0;
loader.y = 0;
SceneManager._scene.addChild(loader);

// キー入力やタッチ操作を無効化する
const _Input_update = Input.update;
Input.update = function() {};
const _TouchInput_update = TouchInput.update;
TouchInput.update = function() {};

fetch(url, {
  method: "POST",
  headers: {"Content-Type": "application/x-www-form-urlencoded"},
  body: `message=${encodeURIComponent(variableValue)}`
})
.then((response) => response.text())
.then((data) => {
  const message = data.replace(/\\n/g, '\n');
  $gameMessage.add(message);

  // ロード画像を非表示にする
  SceneManager._scene.removeChild(loader);

  // キー入力やタッチ操作を有効化する
  Input.update = _Input_update;
  TouchInput.update = _TouchInput_update;

  // 画面を更新する
  SceneManager._scene._waitCount = 0;
})
.catch((error) => console.error(error));
```

これで、回答待ちの間は入力を受け付けず、ローディング画像を表示することが出来る様になります。
![sshot02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/f8ff3f37-f606-ef09-9d06-b5871a77b8f1.jpeg)

# 応用編

デモ動画でツクールMZについて尋ねた時、回答に何行も費やしていました。
この様に、出力する回答が長くなりすぎる事が想定されるので、1ウィンドウで収めるために、PHP側でAPIに投げるsystemロールを使って、回答の文字数を制限しておきます。

1行35文字×4行と考え、140文字もあれば収まるでしょうか。

```php:chatgpt_api.php
$data["messages"][] = array('role' => 'system', 'content' => "140文字以内でまとめてください。");
```

ただ、日本語の文字数に関しては精度が悪い様です。
行指定に関しては無視され、
![sshot 2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/dc77e758-a78a-049c-0a52-a3ef11d4f233.jpeg)
質問によっては全体の文字数指定すら完全に無視してきます。
今後の改善に期待です。
![sshot 1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/6f67292c-b676-ea1b-50fc-85b2e763ec13.jpeg)
ゲーム画面上での文字入力について、チクチクと入力していく作業は微妙なので、あらかじめ選択肢を用意するのも手です。
![sshot01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/f18658ac-0df4-99e9-3a3e-97e335f5693b.jpeg)
上で作ったイベントにて、今度は変数に対して直接日本語を入力してやるだけです。選択肢の内容と、変数は同一である必要はありません。
![sshot.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/4675705b-a149-5570-ad3e-feb543e6c97e.jpeg)
魔王について質問（1回目）
![sshot02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/5b2bea49-6575-dea3-6fba-19b3c6997a38.jpeg)
魔王について質問（2回目）
![sshot03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180923/998d8b5c-326c-96a0-32d1-aadded90634a.jpeg)
尋ねるたびに異なる回答を返してくれるでしょう。

## 注意事項

RPGツクールMZからChatGPT APIへの接続について、わざわざサーバ上のPHPスクリプトを経由することはかなり遠回りな手法に見えます。
もちろんツクールMZだけで直接ChatGPT APIを叩く事だけなら難しくはありません、しかし、そのためにはJavascriptないしプラグインのパラメータや変数など、ゲーム内のどこかに「APIキー」を含める必要があります。

そのままゲームを公開してしまうと、プレイヤーからAPIキーを確認することは非常に容易で、沢山の人からAPIキーを利用されてしまいます。
音楽や画像と違ってJavascriptは暗号化出来ないからです。

これは今回のChatGPT APIに限らず、どのAPIにも言えることで（Googleなどドメインやリファラーで制限をかけられる例もありますが）APIキーは基本、他人に絶対に見られることの無いシークレットな環境に置かれなければなりません。

なので、今後もし「ChatGPT APIが動くよー」的なプラグインが公開されたとしても、それが「APIキーを変数やプラグインパラメータにセットする」様なタイプであれば使用は危険です。
プログラムや変数ごと暗号化出来る機能がツクールのアップデートで備わったとしても、暗号が不可逆であることが保証されない限りは使うべきではないと考えます。