おはようございます
こんにちは
こんばんは
新垣 要伍(アラカキ ヨウゴ)と申します。
Advent Calendar及びQiita初投稿させて頂きます。
記念すべき1発目楽しんでいきます
...... Lets go
当記事の対象者
- ChatGPT(生成AI)に興味がある方
- ChatGPTの活用方法、指示方法がイマイチピンとこない方
- ゲームが好きでプレイされたい方(最後の方にコード記載あります)
目次
1.内容について
2.検証環境
3.ブラウザゲームの基礎を知る
4.いよいよオリジナルゲームへ
5.少しまじめな感想
6.まとめ
7.重要補足
8.サンタゲームのコード
内容について
ざっくり言うと「ブラウザゲームを作ってみた!」になるんですが、
伝えたい着目点としてはゲーム制作ではなく、
ゲーム制作知識ゼロの人間が生成AI(GPT)のみを頼りにゲームを作れるのかになります。
新垣のスペックについて
最終学歴:県内の普通高校を卒業
主な職歴:コールセンター、飲食店
使用可能な業務ツール:Google Workspace関連(主にスプレッドシート(GAS)),Microsoft 365関連(主にExcel),Tableau,PAD
好きなもの:音楽、怪談、激辛
検証環境
- Windows 11
- Google Chrome
- GPT-4
ブラウザゲームの基礎を知る
まずは信頼関係を築くために挨拶。
※優しそうなGPTでひとまず安心
早速ブラウザゲーム制作のための基礎知識を教えていただく
※初心者でも解るように説明を依頼
どういうものが必要か、がなんとなくだけ見えました。
今回目指すゲームはNOT難解なものなのでChromeがあれば出来る方法を教えて頂きます。
何も知らない私を見かねてか、早速具体的な内容を返してくれました、優しい。
指示通りにやってみます!
適用→実行!
なんと!!数当てゲームが爆誕してしました!
以上!祝ブラウザゲーム初制作でした!
それでは、さようなら!!
で、終わるわけ無いですね
GPTの有能さを知ってしまったのでもっと高みを目指していきます!興奮してます。
いよいよオリジナルゲームへ
ゲーム内容を考えた結果、12月なのでサンタゲームを作ってみようと思います!
▼サンタゲームの要件定義
・ゲーム性:プレイヤーがサンタさんを操作し、プレゼントを集めるゲーム
・サンタさんの操作:プレイヤーがマウスカーソルで操作する
・プレゼント:ステージ上を動き回り、壁にぶつかると跳ね返り他の方向に動く
...... Lets go
まずはざっくり依頼
もらった内容を適用→実行!
早速形が返ってきました!
理想までは遠いけど、これだけでもめちゃくちゃワクワクする、、
...... いきます
プレゼントが動かない事と、サンタとカーソル間の乖離に対して修正依頼
適用→実行!
来い!!
ゲーム開始と同時にプレゼントが場外ホームラン
難易度高すぎるク〇ゲーになってしまった
ここで先走ってはダメなので、1個づつ解決していく事にします!
まずは開始と同時にプレゼントがホームランされてしまう件から!
適用→実行!
...... 。。。。
折れずに依頼
適用→実行
きたああああああ!プレゼントが枠からでなくなった!!!
GPTと僕との間で「プレゼントは枠外に出ない」の「枠」の定義齟齬があったようです
やり取りしている内に完璧に意思疎通が出来ていたと思い込んでいた、、
君の事を知っているようで知ろうとしていなかったよ。。
これからはイメージを共有出来るようにしっかり気持ちを伝えていきます!
...... そして
やったああああああああああ!!
とうとう定義通りにサンタゲームを爆誕させる事が出来ました!!
嬉しさと達成感
よし、、
テンションも上がってきたので究極の暇潰しゲーを目指し、更にこだわっていきます!
(暇潰しゲーといえば僕が学生の頃チャリ走ってゲームが流行ってました懐かしい)
...... しばらくして
お待たせしました!!
完成しました!!!!
(ドドンッ)
ゲームらしくルール説明画面も追加
プレイ画面はこんな感じ
ルールは「お邪魔キャラ(毛虫)を避けながらプレゼントをゲットしていく」に落ち着きました。イメージが具現化出来て感無量です。
- 組み込めたゲーム設定
○ゲーム画面(ステージ)の広さは幅:600高さ:500
○スタート画面のデザイン(PLAY、ルール説明ボタン押下時の挙動)
○ルール説明画面のデザイン(文字サイズ、色、配置、戻るボタン設置)
○マウスカーソルとサンタの位置関係
○サンタ、プレゼント、毛虫くんの設定(デザイン、速度、接触判定の設定)
○プレゼント、毛虫くんの条件設定(発生位置(サンタと接触しない位置に発生するように設定))
○プレゼントの消滅時間の変数
○スコアとライフの設定(初期値設定、ライフが0になるとゲームオーバーの設定)
○ゲームオーバー画面の設定(スコアに応じたコメントの設定、リトライ終了ボタン設置)
○ゲームオーバー画面でのリトライ、終了押下時のゲームリセットの設定
少しまじめな感想
今回伝えたい内容はGPTの活用&指示方法なので、そこについて感想を。
「GPT頼りだけ」では限界がどうしても出てくる。基礎知識は必要。
今回苦労した事はやりとりを行うにつれて長文内容となりGPTのエラーが増えた事でした。
一応エラーは必ず出るものでは無かったので、エラー出たら何度か新規会話をやり直す事で回避する事は出来ましたが基礎知識を持ってないが故のGPTへ与える負荷の大きさが今回のループの原因であると考えました。
(今回のコードのトークン数はGPTによると1100程で異常な数値ではないので他要因からの負荷によるエラーっぽいと判断)
GPTへ伝える改修指示 | GPTから貰うコード内容 | |
---|---|---|
知識あり | 改修したい内容を指示 | 修正部分のみでOK |
知識なし | 改修したい内容を指示 | 全文章必要 |
知識なしだと、GPTから修正箇所のコードのみ返答された場合、
コード内のどこを置き換えたらいいか解らないので修正したコードの全文章を送ってもらう
という負担をGPTに与えてしまう事になります。
そのため知識なしの支持者の方がGPTへ与える負荷は大きいと言えると思います。
GPTへは曖昧ではなく、明確な情報を簡潔に提供する
今回序盤でつまづいた「プレゼントが枠を出ないように設定」について。
つまづいた原因はGPTと僕の間の「枠」の認識齟齬なのですが、
GPT(生成AI)は言葉を理解してる訳ではなく直前の言葉の次に来る単語を確率論で計算しているという基本中の基本を改めて認識出来たいい機会だと感じました。
(信じすぎ、ダメ絶対)
また、明確な指示をする事によってで述べたGTPへの負担軽減にも繋がると思います。
まとめ
- GPTを応用する際、どんな内容でも基礎知識は持っておくべき(または活用しながら知識を深めていくべき)
- GPTへは明確かつ簡潔な情報を提供する(NOT曖昧)
- GPTについて様々な注意,理解すべき点はあるが使わない事は確実に勿体ない
と言えるので積極的に自分の周りに取り入れていきましょう!
重要補足
※今回のゲームは知識ゼロの人間が作ったものとなる為、セキュリティリスクへの説明を特記させて頂きます※
今回制作したサンタゲームは、GPT活用上のセキュリティリスクとして挙げられる
生成された情報の真実性・著作権の問題に対してのフォローを行っております。
▶フォロー方法
┗生成された情報の真実性
新規会話のGPTに対してコードを送信。
コード内容を解説してもらう事でリスクに繋がる内容が含まれていないかを確認
┗著作権の問題
新規会話のGPTに対してコードを送信し、
「外部ソースからの転用はないか」
「著作権を侵害する内容が含まれていないか」
などを質問する事で、リスクに繋がる内容が含まれていないかを確認。
※今回は下記のような返答をもらえました
※ゲーム内のデザイン画像の引用元は[いらすとや]です)
今回の内容はあくまで検証用としての内容をGPTに生成してもらいましたが、
実業務や外部へ影響するような内容についての生成を行う場合、
内容を理解出来ないメンバーのみでの判断はトラブルに繋がる可能性がある為、
知見のある方へ相談するなど、慎重に内容を扱いましょう。
サンタゲームのコード
気軽にプレイできるゲームになっているので是非遊んでみてください
下記の▶ コードをクリック→内容をコピー(コード内右上のコピーボタンクリック)
コード
<!DOCTYPE html>
<html>
<head>
<style>
#gameCanvas {
border: 1px solid black;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="600" height="500"></canvas>
<script>
var canvas = document.getElementById("gameCanvas");
var ctx = canvas.getContext("2d");
// ゲームの開始状態を管理するフラグ
var gameStarted = false;
// ゲームオーバー状態を管理するフラグ
var gameOver = false;
// ルール説明画面表示フラグ
var showRules = false;
// プレゼントの生成時間を記録する変数
var ballCreatedTime = Date.now();
// プレゼントが消滅するまでの時間(ミリ秒)
var ballLifeTime = 7000;
// ゲームのスタート画面を描画
function startScreen() {
ctx.fillStyle = "black";
ctx.font = "50px Arial";
ctx.textAlign = "center";
ctx.fillText("サンタゲーム", canvas.width / 2, canvas.height / 2 - 100);
var santaImage = new Image();
santaImage.src = 'https://3.bp.blogspot.com/-Yf14k6kTuDI/UqBHjYh3iMI/AAAAAAAAbaY/WD7JsMotZKk/s800/santa_claus_back.png';
santaImage.onload = function() {
ctx.drawImage(santaImage, canvas.width / 2 - 50, canvas.height / 2 - 50, 100, 100);
}
ctx.font = "30px Arial";
ctx.fillText("PLAY", canvas.width / 2, canvas.height / 2 + 100);
ctx.fillText("ルール説明", canvas.width / 2, canvas.height / 2 + 150); // ルール説明ボタンの追加
}
// ルール説明画面を描画
function rulesScreen() {
ctx.fillStyle = "black";
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = "20px Arial";
ctx.textAlign = "left";
var lineHeight = 30; // 行の高さを設定
var line1 = canvas.height / 2 - 100;
ctx.fillStyle = "blue";
ctx.fillText("▼サンタの操作方法", 50, line1);
ctx.fillStyle = "black";
ctx.fillText("→マウスポインタを動かすとサンタさんが動くよ!", 50, line1 + lineHeight)
ctx.fillStyle = "blue";;
ctx.fillText("▼プレゼントを1個取るとscoreが1増えるよ!", 50, line1 + lineHeight * 2);
ctx.fillStyle = "black";
ctx.fillText("→scoreの上限は無いよ!", 50, line1 + lineHeight * 3);
ctx.fillStyle = "blue";
ctx.fillText("▼プレゼントは発生してから7秒経つと消えるよ!", 50, line1 + lineHeight * 4);
ctx.fillStyle = "black";
ctx.fillText("→プレゼントが1つ無くす毎にLifeが1減るよ!", 50, line1 + lineHeight * 5);
ctx.fillText("→プレゼントを3つ無くすとGAME OVERだよ!", 50, line1 + lineHeight * 6);
ctx.fillStyle = "blue";;
ctx.fillText("▼毛虫くんに触れると即GAME OVERだよ!", 50, line1 + lineHeight * 7);
ctx.fillStyle = "black";
ctx.fillText("→プレゼント3つゲットの度に毛虫くんが増えるよ!", 50, line1 + lineHeight * 8);
ctx.fillStyle = "red";
ctx.fillText("戻る", 50, 50); // "戻る"ボタンの位置を調整
}
// ゲームオーバー画面を描画
function gameOverScreen() {
ctx.fillStyle = "red";
ctx.font = "bold 30px Arial";
ctx.textAlign = "center";
ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2 - 100);
var message;
if (score <= 20) {
message = "坊やだからさ";
} else if (score < 20) {
message = "勇者よ!ここで終わるとは情けない";
} else if (score < 30) {
message = "あきらめたらそこで試合終了だよ";
} else if (score < 40) {
message = "..!?そうかお前あの一族の末裔か";
} else if (score < 50) {
message = "いいでしょう、少々本気を出しますよ";
} else if (score < 60) {
message = "認めよう..お前は史上最強の挑戦者だ";
} else if (score < 70) {
message = "強、、ごめんて";
} else if (score < 80) {
message = "もう、勘弁してください";
} else if (score < 90) {
message = "( ◠‿◠ )ハイ、スゴイスゴーイ";
} else if (score < 100) {
message = "俺に膝をつけさせたのはお前が初めてだ..";
} else if (score >= 100) {
message = "メリークリスマス!!あなたは最強です!!";
}
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
ctx.fillStyle = "green";
ctx.fillText("リトライ", canvas.width / 2 - 100, canvas.height / 2 + 100);
ctx.fillStyle = "black";
ctx.fillText("終了", canvas.width / 2 + 100, canvas.height / 2 + 100);
}
// キャンバスがクリックされたときの処理
function canvasClickHandler(e) {
var rect = e.target.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
if (!gameStarted && !gameOver && !showRules) {
// PLAYボタンの領域をクリックしていたらゲームを開始
if (x > canvas.width / 2 - 50 && x < canvas.width / 2 + 50 && y > canvas.height / 2 + 50 && y < canvas.height / 2 + 100) {
gameStarted = true;
gameInterval = setInterval(draw, 10);
}
// ルール説明ボタンの領域をクリックしていたらルール説明画面を表示
else if (x > canvas.width / 2 - 100 && x < canvas.width / 2 + 100 && y > canvas.height / 2 + 100 && y < canvas.height / 2 + 150) {
showRules = true;
rulesScreen();
}
} else if (showRules) {
// 戻るボタンの領域をクリックしていたらスタート画面に戻る
if (x > 10 && x < 90 && y > 20 && y < 60) {
showRules = false;
ctx.clearRect(0, 0, canvas.width, canvas.height); // 画面をクリア
startScreen();
}
} else if (gameOver) {
// 「リトライ」ボタンの領域をクリックしていたらゲームを再開
if (x > canvas.width / 2 - 150 && x < canvas.width / 2 - 50 && y > canvas.height / 2 + 50 && y < canvas.height / 2 + 150) { // この行を修正
resetGame();
gameStarted = true;
gameOver = false;
gameInterval = setInterval(draw, 10);
} else if (x > canvas.width / 2 && x < canvas.width / 2 + 100 && y > canvas.height / 2 + 50 && y < canvas.height / 2 + 100) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
resetGame();
gameStarted = false;
gameOver = false;
startScreen();
}
}
}
canvas.addEventListener("click", canvasClickHandler, false);
// ここからが元のゲームのコードです
var x = Math.random() * (canvas.width - 20) + 10;
var y = Math.random() * (canvas.height - 20) + 10;
var dx = (Math.random() - 0.5) * 4; // 速度をランダムに設定
var dy = (Math.random() - 0.5) * 4;
var ballRadius = 20; // ボールの半径を倍にする
var ballVisible = true;
var worms = []; // お邪魔キャラの毛虫くんの配列
var wormRadius = 10; // 毛虫くんの半径
var wormImage = new Image();
wormImage.src = 'https://2.bp.blogspot.com/-NNRJIGhcKw8/UfIJQl-MoiI/AAAAAAAAWg4/7KL5NhmbzhA/s800/mushi_kemushi.png';
var paddleWidth = 75;
var paddleHeight;
var paddleX = (canvas.width-paddleWidth) / 2;
var paddleY = (canvas.height-paddleHeight) / 2;
var score = 0;
var lives = 3;
var gameInterval;
var paddleImage = new Image();
paddleImage.src = 'https://3.bp.blogspot.com/-Yf14k6kTuDI/UqBHjYh3iMI/AAAAAAAAbaY/WD7JsMotZKk/s800/santa_claus_back.png';
paddleImage.onload = function() {
var aspectRatio = paddleImage.width / paddleImage.height;
paddleHeight = paddleWidth / aspectRatio;
// ゲームのスタート画面を表示
startScreen();
}
var ballImage = new Image();
ballImage.src = 'https://4.bp.blogspot.com/-HxQIoY1DmOc/VIKm756nDJI/AAAAAAAApZk/JsHPOOKG_sU/s800/christmas_mark2_present.png';
canvas.addEventListener("mousemove", mouseMoveHandler, false);
function mouseMoveHandler(e) {
var relativeX = e.clientX - canvas.offsetLeft;
var relativeY = e.clientY - canvas.offsetTop;
if(relativeX > 0 && relativeX < canvas.width) {
paddleX = relativeX - paddleWidth / 2;
}
if(relativeY > 0 && relativeY < canvas.height) {
paddleY = relativeY - paddleHeight / 2;
}
}
function drawBall() {
if(ballVisible && ballImage.complete) {
ctx.drawImage(ballImage, x - ballRadius, y - ballRadius, ballRadius * 2, ballRadius * 2);
}
}
function drawWorms() {
for(var i = 0; i < worms.length; i++) {
var worm = worms[i];
if(wormImage.complete) {
ctx.drawImage(wormImage, worm.x - wormRadius, worm.y - wormRadius, wormRadius * 2, wormRadius * 2);
}
}
}
function drawPaddle() {
if(paddleImage.complete) {
ctx.drawImage(paddleImage, paddleX, paddleY, paddleWidth, paddleHeight);
}
}
function drawScore() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText("Score: "+score, 30, 20);
}
function drawLives() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText("Life: "+lives, canvas.width-30, 20);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if(gameOver) {
gameOverScreen();
return;
}
drawBall();
drawWorms();
drawPaddle();
drawScore();
drawLives();
// プレゼントが7秒経過したら消滅
if(ballVisible && Date.now() - ballCreatedTime >= ballLifeTime) {
lives--; // プレゼントが消滅したときにライフを減らす
resetBall();
if(lives <= 0) { // ライフが0になったらゲームオーバー
gameOver = true;
clearInterval(gameInterval);
gameOverScreen();
}
}
if(ballVisible && (x + dx > canvas.width-ballRadius || x + dx < ballRadius)) {
dx = -dx;
}
if(ballVisible && (y + dy > canvas.height-ballRadius || y + dy < ballRadius)) {
dy = -dy;
}
if(ballVisible && x > paddleX && x < paddleX + paddleWidth && y > paddleY && y < paddleY + paddleHeight) {
score++;
if(score % 3 === 0) { // スコアが5の倍数になるたびに毛虫くんを追加
addWorm();
}
resetBall();
}
x += dx;
y += dy;
for(var i = 0; i < worms.length; i++) {
var worm = worms[i];
worm.x += worm.dx;
worm.y += worm.dy;
if(worm.x + worm.dx > canvas.width-wormRadius || worm.x + worm.dx < wormRadius) {
worm.dx = -worm.dx;
}
if(worm.y + worm.dy > canvas.height-wormRadius || worm.y + worm.dy < wormRadius) {
worm.dy = -worm.dy;
}
// サンタと毛虫くんが衝突した場合
var distance = Math.sqrt(Math.pow(worm.x - (paddleX + paddleWidth / 2), 2) + Math.pow(worm.y - (paddleY + paddleHeight / 2), 2));
if(distance < wormRadius + paddleWidth / 3) {
gameOver = true;
clearInterval(gameInterval);
gameOverScreen();
}
}
}
function resetBall() {
// プレゼントの初期位置をキャンバスの中央部に近くなるように調整
x = Math.random() * (canvas.width - 20) + 10;
y = Math.random() * (canvas.height - 20) + 10;
// プレゼントがキャンバスの角に近い場合、初期速度を内側に向ける
if (x < canvas.width / 2) {
dx = Math.abs((Math.random() - 0.5) * 4);
} else {
dx = -Math.abs((Math.random() - 0.5) * 4);
}
if (y < canvas.height / 2) {
dy = Math.abs((Math.random() - 0.5) * 4);
} else {
dy = -Math.abs((Math.random() - 0.5) * 4);
}
// プレゼントの生成時間を更新
ballCreatedTime = Date.now();
ballVisible = true;
}
function addWorm() {
var worm;
do {
worm = {
x: Math.random() * (canvas.width - 20) + 20,
y: Math.random() * (canvas.height - 20) + 20,
dx: (Math.random() - 0.5) * 2,
dy: (Math.random() - 0.5) * 2
};
// サンタとの距離を計算
var distance = Math.sqrt(Math.pow(worm.x - (paddleX + paddleWidth / 2), 2) + Math.pow(worm.y - (paddleY + paddleHeight / 2), 2));
} while(distance < wormRadius + paddleWidth); // サンタとの距離が一定値以下なら再度ランダムな位置を生成
worms.push(worm);
}
function resetGame() {
score = 0;
lives = 3;
worms = [];
resetBall();
showRules = false;
}
// ゲームのスタート画面を表示
startScreen();
</script>
</body>
</html>
テキストツール(メモ帳など)に貼り付け→ファイル名を任意の名前.htmlの形式にし、保存
保存したのファイルをブラウザで開く
高得点目指してfight!!