![見出し画像](https://assets.st-note.com/production/uploads/images/154233352/rectangle_large_type_2_81f23d1d512c518c317909262db60f08.jpeg?width=1200)
【ChatGPT o1-mini】インベーダーゲーム:仕様書を作ってからコーディング(遊べます)
はじめに
o1-miniのコーディングの実力を試す。
前回のアステロイドゲームに続き、今回はインベーダーゲームを作ります。
ゲームを作るにあたっては、先にGPT-4oで仕様書を作り、その詳細な仕様書に基づいてo1-miniにコーディングしてもらうことで、より精度の高いゲームを開発します。
と言っても、プロンプトは非常に簡単なのですぐに試していただけます。
すぐに遊んでみたい方は、この画像をクリックすると、サーバにアップしてあるファイルが稼働して遊べます。↓
![](https://assets.st-note.com/img/1726271370-CMPcNxKXj0SohpWrIZFwVs7D.png?width=1200)
仕様書の作成
仕様書作成プロンプト
あなたはプロのゲーム制作者です。ゲーム制作に使用できるレベルの詳細なインベーダーゲームの仕様を書いてください。
仕様書は、GPT-4oで作成します。なぜなら、o1-miniの使用回数は1週間あたり50回に制限されているためです。
GPT-4oは、3時間あたり80回使用できます。
仕様書
インベーダーゲーム仕様書
ゲームの概要
プレイヤーは画面下部の自機を操作し、上空から迫り来る敵(エイリアン)を撃ち落として進行を阻止することが目的の2Dシューティングゲームです。敵を全滅させると次のレベルへ進み、敵の動きが速くなるなど、難易度が徐々に上がっていきます。自機が破壊されるか、敵が画面下部に到達するとゲームオーバーになります。
1. プラットフォーム
対象プラットフォーム: PC, コンソール, モバイル
開発環境: Unity (C#) / Unreal Engine (C++) / Phaser (JavaScript)など
2. 基本システム
2.1. ゲーム画面構成
画面解像度: 1920x1080(HD)を基準とする。アスペクト比は16:9。
エリア: 画面は上から順に「エイリアンエリア」「障害物エリア」「プレイヤーエリア」に分かれる。
エイリアンエリア: 敵が配置され、徐々にプレイヤー側に迫ってくる。
障害物エリア: プレイヤーの弾や敵の弾を遮る障害物が設置されている。障害物は攻撃を受けると徐々に壊れていく。
プレイヤーエリア: プレイヤーの自機が左右に移動し、弾を発射する。
2.2. 操作方法
自機の移動:
左右方向に移動可能(X軸のみ)
キーボードの左右キーまたは、A/Dキーで操作(PCの場合)
モバイルではタッチスライドによる操作
コントローラーの左右スティック、またはD-padで移動(コンソール)
射撃:
プレイヤーは1種類の弾を発射可能。
キーボードのスペースキーまたはコントローラーのボタンで射撃。
モバイルではタッチ操作による射撃ボタンを配置。
2.3. 敵(エイリアン)
配置: 敵は5行×11列(計55体)のマトリクス状に配置される。
移動パターン: 敵は一斉に左右に移動し、端に達すると縦に1段下がる。移動速度は敵の数が減るごとに速くなる。
攻撃: ランダムなタイミングで、一定の敵が下方向に弾を発射する。
種類: 敵は複数種類存在し、得点や外見が異なる。
標準エイリアン: 一般的な敵。基本得点。
強化エイリアン: 移動速度や耐久力が高い。高得点。
UFO(ボーナス敵): 上部を横切る特殊な敵。プレイヤーが撃破すると高得点。
3. システム仕様
3.1. ラウンド進行
ラウンドクリア条件: すべての敵を撃破するとラウンドクリア。
次ラウンドへの移行: 敵の数や速度、攻撃頻度が増加する次のラウンドに進む。
難易度上昇要素:
敵の移動速度が増加。
敵の弾発射頻度が上昇。
障害物の耐久力が低下。
3.2. ゲームオーバー条件
敵が画面下部まで到達した場合。
プレイヤーの自機がすべて破壊された場合(ライフが0になる)。
ゲームオーバー時にはスコアとメインメニューへの遷移が表示される。
3.3. ライフシステム
初期ライフ: プレイヤーは初期状態で3ライフを持つ。
ライフ減少: 敵の弾や敵自体に自機が接触するとライフが1つ減少。
ライフ追加: 一定のスコアに達するとライフが1つ追加される。
3.4. スコアシステム
基本スコア: 敵を撃破するごとに得点が加算される。
標準エイリアン: 100点
強化エイリアン: 200点
UFO: 500点
ハイスコア: ゲームオーバー後にハイスコアが保存され、メインメニューに表示。
3.5. 障害物
配置: 画面中ほどに4つの障害物が設置されている。
破壊: 障害物はプレイヤーと敵の弾で破壊される。各障害物には耐久力が設定されており、一定回数の攻撃で消滅する。
再生成: 新しいラウンド開始時に障害物は再生成される。
4. ビジュアルおよびサウンド
4.1. ビジュアル
グラフィックスタイル: レトロなピクセルアート風のデザイン。または、モダンな2Dスタイル。
エフェクト: 弾の発射、敵の爆発、自機の破壊時には専用のエフェクト(閃光、爆発)が表示される。
4.2. サウンド
BGM: ラウンドごとに異なるBGMがループ再生される。敵の数が減るとBGMが速くなる演出。
効果音:
弾発射音、敵撃破音、自機破壊音
UFO出現時の特殊効果音
ボリュームコントロール: BGMと効果音はそれぞれ個別にオン/オフおよびボリューム調整が可能。
5. UI設計
5.1. メインメニュー
項目:
プレイ開始
オプション(サウンド設定、操作設定)
ハイスコア
終了
5.2. ゲーム画面UI
スコア表示: 画面上部に現在のスコアとハイスコアを表示。
ライフ表示: 画面左下に残りライフをアイコンで表示。
ラウンド表示: 現在のラウンドを右上に表示。
6. 拡張機能
6.1. マルチプレイヤーモード
協力プレイ: 2人のプレイヤーが同時に参加し、協力して敵を撃破する。ライフとスコアは共有。
対戦プレイ: 2人のプレイヤーが別々に敵を倒し、どちらが多くのスコアを稼げるか競うモード。
6.2. スペシャルウェポン
特定の条件を満たすと、自機に強力な武器(連射可能なレーザー、爆発弾など)が一時的に追加される。
7. 技術仕様
7.1. フレームレート
目標フレームレート: 60fps
7.2. 物理エンジン
基本的な当たり判定は矩形(AABB)で処理。
弾と敵、弾と障害物、自機と敵の衝突判定。
7.3. 外部ライブラリ
パーティクルエフェクト: エフェクト強化のために外部ライブラリ(例: Unityのパーティクルシステム)を使用。
この仕様書を基に、ゲームの設計、プログラミング、グラフィックデザイン、サウンドデザインなど各フェーズに進行できます。また、ゲームバランスや追加要素は開発途中でテスト・調整を繰り返しながら仕上げる形となります。
o1-miniにモデルを切り替え
次に、モデルを切り替えて以下のプロンプトを入力します。今回は、Webアプリケーションとして作りたいので、プロンプトではそのような指定をしています。
プロンプト
上記の使用をもとに、HTML、CSS、JSでできる範囲で、コードを実装してください。シングルHTMLファイルアプリケーションで作ります。
o1-miniの出力
以下に、シングルHTMLファイルとして実装可能なインベーダーゲームのコードを提供します。このコードは、HTML、CSS、JavaScriptを使用して基本的なゲーム機能を実現しています。以下のコードをコピーして `.html` ファイルとして保存し、ブラウザで開くことでゲームをプレイできます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>インベーダーゲーム</title>
<style>
/* リセットスタイル */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ゲームキャンバスのスタイル */
body {
background-color: #000;
color: #fff;
font-family: Arial, sans-serif;
overflow: hidden;
}
#gameCanvas {
display: block;
margin: 0 auto;
background-color: #000;
}
/* メインメニューのスタイル */
#mainMenu {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
#mainMenu button {
display: block;
width: 200px;
margin: 10px auto;
padding: 10px;
font-size: 18px;
cursor: pointer;
}
/* ゲームオーバー画面のスタイル */
#gameOverScreen {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
display: none;
}
#gameOverScreen button {
display: block;
width: 200px;
margin: 10px auto;
padding: 10px;
font-size: 18px;
cursor: pointer;
}
/* スコア表示のスタイル */
#scoreBoard {
position: absolute;
top: 10px;
left: 10px;
font-size: 18px;
}
/* ライフ表示のスタイル */
#lifeBoard {
position: absolute;
top: 10px;
right: 10px;
font-size: 18px;
}
/* ラウンド表示のスタイル */
#roundBoard {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
}
</style>
</head>
<body>
<!-- メインメニュー -->
<div id="mainMenu">
<h1>インベーダーゲーム</h1>
<button onclick="startGame()">プレイ開始</button>
<button onclick="showHighScore()">ハイスコア</button>
<button onclick="closeGame()">終了</button>
</div>
<!-- ゲームオーバー画面 -->
<div id="gameOverScreen">
<h1>ゲームオーバー</h1>
<p id="finalScore">スコア: 0</p>
<button onclick="restartGame()">もう一度プレイ</button>
<button onclick="returnToMenu()">メインメニューへ</button>
</div>
<!-- スコア、ライフ、ラウンド表示 -->
<div id="scoreBoard">スコア: 0</div>
<div id="lifeBoard">ライフ: ♥♥♥</div>
<div id="roundBoard">ラウンド: 1</div>
<!-- ゲームキャンバス -->
<canvas id="gameCanvas" width="800" height="600"></canvas>
<script>
// ゲームの初期設定
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const mainMenu = document.getElementById('mainMenu');
const gameOverScreen = document.getElementById('gameOverScreen');
const finalScoreElem = document.getElementById('finalScore');
const scoreBoard = document.getElementById('scoreBoard');
const lifeBoard = document.getElementById('lifeBoard');
const roundBoard = document.getElementById('roundBoard');
let gameInterval;
let enemyShootInterval;
let ufoInterval;
// ゲームの状態
let gameState = 'menu'; // 'menu', 'playing', 'gameover'
// プレイヤーの設定
const player = {
width: 50,
height: 30,
x: canvas.width / 2 - 25,
y: canvas.height - 50,
speed: 5,
dx: 0,
bullets: [],
canShoot: true,
shootCooldown: 500, // ミリ秒
lives: 3,
score: 0,
round: 1
};
// 敵の設定
const enemyRows = 5;
const enemyCols = 11;
const enemyWidth = 40;
const enemyHeight = 30;
const enemyPadding = 20;
const enemyOffsetTop = 50;
const enemyOffsetLeft = 50;
let enemies = [];
let enemyDirection = 1; // 1: 右, -1: 左
let enemySpeed = 1;
// UFOの設定
const ufos = [];
const ufoSpeed = 2;
// キー入力の管理
const keys = {
left: false,
right: false,
space: false
};
// 高スコアの管理
let highScore = localStorage.getItem('highScore') || 0;
// ゲームの初期化
function init() {
// 初期スコアとライフ
player.score = 0;
player.lives = 3;
player.round = 1;
// 初期敵の配置
enemies = [];
for(let row = 0; row < enemyRows; row++) {
for(let col = 0; col < enemyCols; col++) {
const enemy = {
x: enemyOffsetLeft + col * (enemyWidth + enemyPadding),
y: enemyOffsetTop + row * (enemyHeight + enemyPadding),
width: enemyWidth,
height: enemyHeight,
type: 'standard',
alive: true
};
enemies.push(enemy);
}
}
// 初期スピード
enemySpeed = 1;
// UFOの初期化
ufos.length = 0;
}
// メインゲームループ
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
update();
draw();
if (player.lives <= 0 || enemiesReachedBottom()) {
endGame();
}
gameInterval = requestAnimationFrame(gameLoop);
}
// 更新処理
function update() {
// プレイヤーの移動
player.x += player.dx;
// 境界チェック
if (player.x < 0) player.x = 0;
if (player.x + player.width > canvas.width) player.x = canvas.width - player.width;
// プレイヤーの射撃
if (keys.space && player.canShoot) {
player.bullets.push({
x: player.x + player.width / 2 - 2.5,
y: player.y,
width: 5,
height: 10,
dy: -7
});
player.canShoot = false;
setTimeout(() => {
player.canShoot = true;
}, player.shootCooldown);
}
// 更新プレイヤーの弾
player.bullets.forEach((bullet, index) => {
bullet.y += bullet.dy;
// 弾が画面外に出たら削除
if (bullet.y + bullet.height < 0) {
player.bullets.splice(index, 1);
}
});
// 敵の移動
let shiftDown = false;
enemies.forEach(enemy => {
if (enemy.alive) {
enemy.x += enemySpeed * enemyDirection;
// 画面端に達したら下に移動し方向を反転
if (enemy.x + enemy.width > canvas.width - enemyOffsetLeft || enemy.x < enemyOffsetLeft) {
shiftDown = true;
}
}
});
if (shiftDown) {
enemyDirection *= -1;
enemies.forEach(enemy => {
if (enemy.alive) {
enemy.y += enemyHeight;
}
});
}
// 敵の自動射撃
if (!enemyShootInterval) {
enemyShootInterval = setInterval(enemyShoot, 1000);
}
// 更新敵の弾
enemies.forEach(enemy => {
if (enemy.alive && Math.random() < 0.001 * player.round) { // 射撃確率をラウンドに応じて増加
enemyBullets.push({
x: enemy.x + enemy.width / 2 - 2.5,
y: enemy.y + enemy.height,
width: 5,
height: 10,
dy: 4
});
}
});
// 敵の弾の更新
enemyBullets.forEach((bullet, index) => {
bullet.y += bullet.dy;
if (bullet.y > canvas.height) {
enemyBullets.splice(index, 1);
}
});
// UFOの移動
ufos.forEach((ufo, index) => {
ufo.x += ufo.dy;
if (ufo.x > canvas.width) {
ufos.splice(index, 1);
}
});
// 障害物の追加や他の要素はここに追加できます
// 衝突判定
checkCollisions();
// 更新UI
updateUI();
}
// 描画処理
function draw() {
// プレイヤーの描画
ctx.fillStyle = '#00f';
ctx.fillRect(player.x, player.y, player.width, player.height);
// プレイヤーの弾の描画
ctx.fillStyle = '#fff';
player.bullets.forEach(bullet => {
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
});
// 敵の描画
ctx.fillStyle = '#0f0';
enemies.forEach(enemy => {
if (enemy.alive) {
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
}
});
// 敵の弾の描画
ctx.fillStyle = '#f00';
enemyBullets.forEach(bullet => {
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
});
// UFOの描画
ctx.fillStyle = '#ff0';
ufos.forEach(ufo => {
ctx.fillRect(ufo.x, ufo.y, ufo.width, ufo.height);
});
}
// 衝突判定
function checkCollisions() {
// プレイヤーの弾と敵の衝突
player.bullets.forEach((bullet, bIndex) => {
enemies.forEach((enemy, eIndex) => {
if (enemy.alive && isColliding(bullet, enemy)) {
enemy.alive = false;
player.score += 100; // 標準エイリアンの得点
player.bullets.splice(bIndex, 1);
}
});
});
// 敵の弾とプレイヤーの衝突
enemyBullets.forEach((bullet, bIndex) => {
if (isColliding(bullet, player)) {
player.lives -= 1;
enemyBullets.splice(bIndex, 1);
}
});
// 敵とプレイヤーの衝突
enemies.forEach(enemy => {
if (enemy.alive && isColliding(enemy, player)) {
player.lives = 0;
}
});
// UFOとプレイヤーの衝突
ufos.forEach((ufo, uIndex) => {
if (isColliding(ufo, player)) {
player.lives = 0;
}
});
// 全ての敵が倒された場合
if (enemies.every(enemy => !enemy.alive)) {
player.round += 1;
init(); // 新しいラウンドの初期化
}
}
// 衝突判定のヘルパー関数
function isColliding(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
// 敵の射撃処理
let enemyBullets = [];
function enemyShoot() {
enemies.forEach(enemy => {
if (enemy.alive && Math.random() < 0.01) { // 射撃確率
enemyBullets.push({
x: enemy.x + enemy.width / 2 - 2.5,
y: enemy.y + enemy.height,
width: 5,
height: 10,
dy: 4
});
}
});
}
// UFOの生成
function spawnUFO() {
const ufo = {
x: -50,
y: 50 + Math.random() * 100,
width: 50,
height: 20,
dy: ufoSpeed
};
ufos.push(ufo);
}
// ランダムにUFOを生成
function startUFOs() {
ufoInterval = setInterval(spawnUFO, 10000); // 10秒ごとにUFO出現
}
// ランダムにUFOを生成し始める
startUFOs();
// 敵が画面下部に到達したかの判定
function enemiesReachedBottom() {
return enemies.some(enemy => enemy.alive && enemy.y + enemy.height >= player.y);
}
// UIの更新
function updateUI() {
scoreBoard.textContent = `スコア: ${player.score}`;
lifeBoard.textContent = `ライフ: ${'♥'.repeat(player.lives)}`;
roundBoard.textContent = `ラウンド: ${player.round}`;
}
// ゲームの開始
function startGame() {
gameState = 'playing';
mainMenu.style.display = 'none';
gameOverScreen.style.display = 'none';
init();
gameLoop();
}
// ゲームの終了
function endGame() {
gameState = 'gameover';
cancelAnimationFrame(gameInterval);
clearInterval(enemyShootInterval);
clearInterval(ufoInterval);
enemyShootInterval = null;
ufoInterval = null;
// ハイスコアの更新
if (player.score > highScore) {
highScore = player.score;
localStorage.setItem('highScore', highScore);
}
// ゲームオーバー画面の表示
finalScoreElem.textContent = `スコア: ${player.score}`;
gameOverScreen.style.display = 'block';
}
// ゲームの再スタート
function restartGame() {
gameOverScreen.style.display = 'none';
startGame();
}
// メインメニューに戻る
function returnToMenu() {
gameOverScreen.style.display = 'none';
mainMenu.style.display = 'block';
}
// 高スコア表示(仮)
function showHighScore() {
alert(`ハイスコア: ${highScore}`);
}
// ゲームを閉じる(仮)
function closeGame() {
alert('ゲームを終了します。');
}
// キー入力のイベントリスナー
document.addEventListener('keydown', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') {
keys.left = true;
player.dx = -player.speed;
}
if (e.code === 'ArrowRight' || e.code === 'KeyD') {
keys.right = true;
player.dx = player.speed;
}
if (e.code === 'Space') {
keys.space = true;
}
});
document.addEventListener('keyup', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') {
keys.left = false;
if (keys.right) {
player.dx = player.speed;
} else {
player.dx = 0;
}
}
if (e.code === 'ArrowRight' || e.code === 'KeyD') {
keys.right = false;
if (keys.left) {
player.dx = -player.speed;
} else {
player.dx = 0;
}
}
if (e.code === 'Space') {
keys.space = false;
}
});
// タッチ入力の管理(モバイル対応)
let touchStartX = null;
canvas.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
touchStartX = touch.clientX;
});
canvas.addEventListener('touchmove', (e) => {
if (touchStartX !== null) {
const touch = e.touches[0];
const deltaX = touch.clientX - touchStartX;
player.x += deltaX * 0.1; // 移動量を調整
touchStartX = touch.clientX;
// 境界チェック
if (player.x < 0) player.x = 0;
if (player.x + player.width > canvas.width) player.x = canvas.width - player.width;
}
});
canvas.addEventListener('touchend', (e) => {
touchStartX = null;
});
// タッチによる射撃
// 画面右下に射撃ボタンを配置
const shootButton = document.createElement('button');
shootButton.textContent = '射撃';
shootButton.style.position = 'absolute';
shootButton.style.bottom = '20px';
shootButton.style.right = '20px';
shootButton.style.padding = '10px 20px';
shootButton.style.fontSize = '16px';
shootButton.style.opacity = '0.5';
shootButton.style.backgroundColor = '#fff';
shootButton.style.border = 'none';
shootButton.style.borderRadius = '5px';
shootButton.style.cursor = 'pointer';
shootButton.style.display = 'none'; // 初期は非表示
document.body.appendChild(shootButton);
// タッチデバイスの場合のみ表示
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
shootButton.style.display = 'block';
shootButton.addEventListener('touchstart', () => {
keys.space = true;
});
shootButton.addEventListener('touchend', () => {
keys.space = false;
});
}
// ウィンドウのリサイズに対応
window.addEventListener('resize', () => {
// 必要に応じてキャンバスサイズを調整
});
// ゲームの開始
init();
</script>
</body>
</html>
コードの説明
1. HTML構造
メインメニュー: ゲーム開始、ハイスコア表示、終了ボタンを含む。
ゲームオーバー画面: スコア表示と再スタート、メインメニューに戻るボタンを含む。
スコア、ライフ、ラウンド表示: ゲーム中にリアルタイムで更新される情報。
ゲームキャンバス: ゲームの描画領域。
射撃ボタン: モバイルデバイス向けに画面右下に配置。
2. CSSスタイル
リセットスタイル: マージンやパディングをリセット。
キャンバスとUI要素のスタイル: 中央配置、フォント設定、色設定など。
ボタンのスタイル: 見やすく操作しやすいデザイン。
3. JavaScript機能
ゲームの初期設定: プレイヤー、敵、弾、UFOの設定。
メインゲームループ (`gameLoop`): ゲームの更新と描画を繰り返す。
更新処理 (`update`):
プレイヤーの移動と射撃。
敵の移動と射撃。
UFOの生成と移動。
衝突判定。
UIの更新。
描画処理 (`draw`): プレイヤー、敵、弾、UFOをキャンバスに描画。
衝突判定 (`checkCollisions`): 弾と敵、弾とプレイヤー、敵とプレイヤーの衝突を検出。
UIの更新 (`updateUI`): スコア、ライフ、ラウンドを表示。
イベントリスナー:
キーボード入力(左右移動、射撃)。
タッチ入力(モバイルデバイス向けのスライド移動と射撃ボタン)。
ゲームの状態管理: メインメニュー、プレイ中、ゲームオーバーの切り替え。
4. 拡張可能なポイント
敵の種類: 標準エイリアン以外の強化エイリアンやUFOの種類を追加。
障害物の追加: プレイヤーと敵の弾を遮る障害物を設置し、耐久力を持たせる。
スコアシステムの拡張: 特定の条件でスコアボーナスやライフ追加を実装。
パワーアップ: スペシャルウェポンやライフ増加などのパワーアップアイテムを追加。
マルチラウンドの管理: ラウンドが進むごとに敵の数や速度を調整。
音声効果: 効果音やBGMを追加してゲームの雰囲気を向上。
この基本的な実装を基に、さらに詳細な機能やビジュアル、サウンドを追加することで、仕様に沿ったプロフェッショナルなインベーダーゲームを作成することができます。
使い方
このコードを、メモ帳を開いて"invador.html"と名前をつけて保存すると、HTMLファイルが出来上がりますので、これをクリックするとブラウザが開いて、ゲームが始まります。
上記のコードを元に、画面を縦長に修正した完成版のファイルはこちらです。ダウンロードして遊んでみてください。
このファイルをそのままサーバにあっぷしたものはこちら。以下をクリックするとダウンロードしなくても遊べます。
まとめ
このコードの文字数は、17300文字でした。
GPT-4oは、4000字程度の出力なので、o1-miniでは、かなり出力の分量が増えました。
それによって、コードのクオリティも上がったのかもしれません。
【重版決定】AIアプリをプログラミング不要で開発する書籍
GPTsを自分で作って世界に公開したい方はこちら!