はじめに
- この記事はLLM・LLM活用Advent Calendar 13日目の記事です。
- 生成AIを活用し、ゲームのストーリーやキャラクター、画像の生成を行い、それを基にビジュアルノベルを制作します。
- 最後には、生成AIがユーザーの選択に応じて自動的かつ動的にゲームを制作する応用例についてもご紹介します。
ビジュアルノベルとはどういうゲームジャンルなのか?
画像はPS Vita版「Fate/stay night [Realta Nua]」
-
ビジュアルノベルは、テキストに絵や音楽、選択肢などを組み合わせた独特の電子小説形式で、プレイヤーに物語を読むだけでなく、体験させることを目的としています。
-
このジャンルは1980年代後半から1990年代初頭に日本で誕生し、現在では世界中で人気を集めています。
人気ビジュアルノベル作品
Ren’Pyについて知ってました?
-
Ren'Pyは、世界中で数千人のクリエーターに愛用されている、ビジュアルノベル制作に特化したオープンソースのゲームエンジンです。
-
簡単なスクリプト言語を使用しながら、多言語対応やマルチプラットフォーム展開が可能な点が特徴です。
Ren'Pyの主な特徴
Ren'Pyの主な特徴として、以下の点が挙げられます:
- 脚本形式の簡単なスクリプト構文を採用し、初心者でも理解しやすい設計
- 多言語対応が容易で、翻訳ファイルの作成だけでゲーム内テキストを切り替え可能
- Windows、macOS、Linux、Android、iOSなど、複数のプラットフォームに対応
- MITライセンスのもとで提供され、商用利用も自由
- ストーリー分岐、セーブ・ロード機能、ロールバック機能など、ビジュアルノベルに必要な機能を標準搭載
これらの特徴により、Ren'Pyは初心者から上級者まで幅広い層のクリエーターに支持され、グローバル展開にも適したツールとなっています。
Ren'Py製の有名ゲーム
Ren'Pyを使用して制作された有名なゲームには、以下のようなタイトルがあります:
- 『ドキドキ文芸部!』- 日本でも人気を博した心理ホラーゲーム
- 『Corpse Party D2: Zero Hope』- ホラーアドベンチャーゲームシリーズの一作
- 『Digital: A Love Story』- レトロなコンピューター画面を模したビジュアルノベル
- 『Analogue: A Hate Story』- SF設定のビジュアルノベル
- 『Don't take it personally, babe, it just ain't your story』- 学園を舞台にしたストーリー
これらの作品は、Ren'Pyの柔軟性と表現力を活かし、独創的なゲーム体験を提供しています。多様なジャンルやスタイルのゲームが制作可能であることが、Ren'Pyの魅力の一つとなっています。
Ren’Pyを触ってみよう!
インストール
- https://www.renpy.org/ よりファイルをダウンロード
スクリプトファイルの構造
Ren'Pyのスクリプトファイルは.rpy拡張子を持ち、script.rpyがデフォルトのスクリプトファイルです。
基本構造
label start:
"ここにストーリーを記述します。"
return
主な文法要素
ラベル (Label)
- ラベルはゲーム内の特定のポイント(例: シーン切り替え)を示し、labelキーワードで宣言します。
- すべてのRen'Pyスクリプトはstartラベルから開始されます。
label start:
"ゲームが始まりました。"
jump ending # 特定のラベルにジャンプ
label ending:
"ゲームが終了しました。"
return
ダイアログ (Dialogue)
-
ダイアログは引用符(")で囲んで記述し、セリフを画面に表示します。
-
キャラクター名を指定してセリフを表示することも可能です。
"こんにちは、Ren'Pyへようこそ!"
キャラクター定義後のダイアログ
define e = Character("エミリー")
label start:
e "こんにちは!私はエミリーです。"
return
シーンの切り替え (Scene)
-
scene
キーワードを使って背景を設定したり、シーンを切り替えたりします。 -
with
キーワードで切り替え効果を指定できます。
scene bg room
with fade
キャラクター定義 (Define)
-
define
キーワードでキャラクターを定義し、名前や色、スタイルを設定します。
define s = Character("サヨ", color="#00FF00")
選択肢 (Menu)
-
menu
を使用してプレイヤーに選択肢を提供します。
menu:
"選択肢1":
"あなたは選択肢1を選びました。"
"選択肢2":
"あなたは選択肢2を選びました。"
音楽と効果音
-
play
とstop
キーワードを使用して音楽や効果音を制御します。
play music "bgm.ogg"
stop music
画像の表示
-
show
を使って画像を画面に表示し、hideで削除します。
show character happy
hide character
変数と条件文
- 変数を設定し、条件文(
if
、elif
、else
)を使用できます。
$ points = 0
if points > 5:
"あなたは高得点を獲得しました!"
else:
"もっとポイントを集めてください。"
実行の流れ
-
label start:
から開始します。 -
スクリプトに記載された順番に実行されます。
-
特定のラベルに移動する場合は
jump
を使用します。 -
ゲームを終了する場合は
return
を使用します。
基本文法のまとめ
キーワード | 役割 |
---|---|
label |
ラベルの宣言 |
scene |
背景画像の設定と切り替え |
show |
キャラクターまたは画像の表示 |
hide |
キャラクターまたは画像の非表示 |
menu |
選択肢の提供 |
play |
音楽または効果音の再生 |
stop |
音楽または効果音の停止 |
$ |
変数またはPythonコードの実行 |
return |
ゲーム終了または現在のラベルの終了 |
生成AIを使って簡単なゲームを作ってみよう!
ストーリーの生成
- ストーリー生成用のプロンプト
美少女恋愛シミュレーションのストーリーをRen'Pyスクリプト形式で作成してください。以下の条件を満たすよう、豊富で詳細なストーリーを構成してください。
## 1. 登場人物
- 主人公である男女2人を登場させる。
- 各キャラクターの性格、背景、目標を具体的に設定し、キャラクターに深みを持たせる。
## 2. ストーリー構成
- **起承転結**がはっきりと分かるようにストーリーを展開する。
- **起**: 主人公同士の出会いと日常の始まり。
- **承**: お互いへの感情の発展と葛藤要素の導入。
- **転**: 緊張感あふれる事件や秘密が明らかになり、クライマックスを形成する。
- **結**: 衝撃的などんでん返しでストーリーを締めくくる。
## 3. どんでん返しの要素
- ストーリーの最後に予想外の**衝撃的などんでん返し**を含める。
- どんでん返しはストーリー全体の流れと矛盾しないようにしつつ、プレイヤーに強い印象を与えるものにする。
## 4. 没入感と緊張感
- プレイヤーがストーリーに没入できるように、**詳細な描写**や**感情表現**を豊かにする。
- 会話と選択肢を活用して緊張感を高め、プレイヤーの参加感を向上させる。
## 5. Ren'Pyスクリプト形式
- 実際にRen'Pyエンジンで実行可能な**スクリプト形式**で作成する。
- **ラベル(label)**、**キャラクター定義(define)**、**ダイアログ(dialogue)**、**選択肢(menu)**などを適切に活用する。
- 背景画像、音楽、効果音などの**マルチメディア要素**も含め、リアリティのある演出を行う。
## 6. 分量と詳細さ
- ストーリーを**非常に長く作成**し、十分なボリュームを確保する。
- 各シーンや状況について詳細な説明や会話を盛り込む。
- 生成されたRen'Pyスクリプト
define mc = Character("主人公")
define maya = Character("マヤ", color="#FF69B4")
define akira = Character("アキラ", color="#87CEFA")
label start:
scene bg school_day
with fade
mc "新しい学校に転校してきた初日、僕の胸は期待と不安でいっぱいだった。"
mc "クラスメイトたちは僕をちらりと見てすぐに会話に戻る。静かな教室にいるのが少し気まずい。"
# マヤの登場
show maya normal at left
with fade
maya "ねえ、新しい生徒さん?"
mc "(クラスで一番目立つ子が話しかけてきた。彼女の名前はマヤ...かな?)"
mc "あ、そうだよ。今日からよろしくお願いします。"
maya "私はマヤ。なんでも聞いてね!"
mc "ありがとう、助かるよ。"
maya "ねえ、せっかくだし、放課後どこかに寄っていかない?"
mc "(彼女の明るい笑顔に、断る理由なんて思いつかなかった。)"
# シーン転換: 屋上
scene bg rooftop_day
with fade
mc "放課後、マヤと一緒に学校の屋上に来た。"
mc "眺めのいい場所で、風が気持ちいい。"
maya "ここ、いいでしょ?私の秘密の場所なの。"
mc "(彼女の声には少しの寂しさが混じっているように聞こえた。)"
mc "いい場所だね。でも、どうして秘密なんだ?"
maya "...いろいろあるのよ。"
maya "でも、あなたになら少し話してもいいかな。"
# 選択肢: マヤの秘密を聞くか否か
menu:
"彼女の秘密を聞く":
jump hear_secret
"深く聞かない":
jump avoid_secret
label hear_secret:
maya "実はね..."
maya "私、昔から普通の人とは少し違うの。"
maya "それが原因で、たくさんの人とすれ違ってきた。"
mc "(彼女の表情は複雑だ...彼女が抱える孤独に、僕は何と言えばいいのか分からない。)"
mc "違いがあっても、それは君の特別な部分じゃないかな?"
maya "…ありがとう。そう言ってくれる人、初めてかも。"
jump story_progress
label avoid_secret:
mc "そんなに大切な場所なら、無理に話さなくてもいいよ。"
maya "ふふ、ありがとう。でも、いつか話すかも。"
jump story_progress
label story_progress:
# 新キャラクター登場: アキラ
scene bg classroom
with fade
show akira normal at right
with moveinright
mc "翌日、新しいクラスメイトと自己紹介を交わす中で、もう一人の存在が目に入った。"
akira "君が昨日転校してきた人か。"
mc "ああ、そうだよ。君は...?"
akira "俺はアキラ。このクラスにいる限り、よろしくな。"
mc "(アキラの声は冷たく、どこか挑戦的だ。)"
mc "(彼とマヤは親しい関係のようだけど、何か隠しているような気がする。)"
# 選択肢: アキラと距離を縮めるか
menu:
"アキラと友好的に接する":
jump befriend_akira
"アキラを警戒する":
jump distrust_akira
label befriend_akira:
mc "アキラ、よかったら放課後どこか行かないか?"
akira "俺に?...まあ、時間があればな。"
mc "(少しずつ彼の態度が柔らかくなっていくのを感じた。)"
jump further_story
label distrust_akira:
mc "(彼の態度はどうも信用できない。必要以上に関わらない方が良さそうだ。)"
akira "(くすりと笑う)...まあ、これからだな。"
jump further_story
label further_story:
# 緊張感のある事件の発生
scene bg park_night
with fade
mc "数日後、学校の帰り道、公園でマヤとアキラの口論に遭遇した。"
mc "(彼らの言い争いは激しく、ただ事ではない。)"
maya "あなたには関係ない!"
akira "そう思うか?全部話すべきだと思うけどな。"
mc "(彼らの秘密とは何なんだ?)"
mc "一体何が起きているんだ!"
maya "あなたにはまだ話せないわ。でも、真実を知る覚悟があるなら..."
# 選択肢: 真実を受け入れるか
menu:
"真実を受け入れる":
jump accept_truth
"現状を維持する":
jump remain_in_bliss
label accept_truth:
scene bg digital
with fade
mc "目の前の風景が崩れていく。"
mc "...僕が見ていたこの世界は何だったんだ?"
mc "(すべてが仮想現実だったと知った時、僕は...。)"
return
label remain_in_bliss:
scene bg park_night
with fade
mc "僕はこの世界で生きていきたい。たとえそれが偽りのものであっても。"
maya "ありがとう、あなたがいてくれて嬉しい。"
mc "(それでも、この場所が僕の選んだ現実だ。)"
return
キャラクター画像と背景画像の生成
- 使用ツール
- キャラクター画像生成用のプロンプト
Stable Diffusionでキャラクター画像を生成するためのプロンプトを作成してください。
1. 各キャラクターが一貫性のあるスタイルを持つようにプロンプトを作成する
2. スクリプトに記載された状態(例:通常、笑顔など)ごとに生成する
3. 英語で記述する
4. プロンプトのみを返す
- 背景画像の生成
Stable Diffusionで背景画像を生成するためのプロンプトを作成してください。
デモプレイ動画
- 「現実の欠片(Fragments of Reality)」
生成AIをダイナミックに生成してみよう!
やりたいこと
- ユーザーの選択をコンテキストで管理しながら、自動で次のストーリーと画像が生成されるようにしたい
実装済みのコード
init python:
# 必要なライブラリのインポート
import urllib.request
import json
import ssl
import os
import time
import base64
class StoryGenerator:
"""
OpenAI APIを使用してストーリーと画像を生成するクラス
"""
def __init__(self):
"""
StoryGeneratorの初期化
- コンテキスト履歴の初期化
- API認証情報の設定
- SSLコンテキストの設定
"""
self.context = []
self.API_KEY = "YOUR_API_KEY"
self.headers = {
"Authorization": f"Bearer {self.API_KEY}",
"Content-Type": "application/json"
}
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
def generate_image(self, prompt):
"""
DALL-E 3を使用して画像を生成する
Args:
prompt (str): 画像生成のためのプロンプト
Returns:
str: 生成された画像のファイルパス、またはエラーメッセージ
"""
image_dir = os.path.join(renpy.config.gamedir, "images")
# DALL-E 3のリクエストデータを設定
data = {
"model": "dall-e-3",
"prompt": f"ANIME STYLE, {prompt}",
"n": 1,
"size": "1024x1024",
"quality": "hd"
}
try:
# OpenAI APIに画像生成リクエストを送信
req = urllib.request.Request(
"https://api.openai.com/v1/images/generations",
data=json.dumps(data).encode('utf-8'),
headers=self.headers,
method='POST'
)
with urllib.request.urlopen(req, context=self.ssl_context) as response:
result = json.loads(response.read().decode('utf-8'))
if 'data' in result and len(result['data']) > 0:
image_url = result['data'][0]['url']
# 生成された画像をローカルに保存
timestamp = int(time.time())
filename = f"generated_image_{timestamp}.png"
filepath = os.path.join(image_dir, filename)
return_filepath = os.path.join("images", filename)
image_req = urllib.request.Request(image_url)
with urllib.request.urlopen(image_req, context=self.ssl_context) as img_response:
with open(filepath, 'wb') as f:
f.write(img_response.read())
return return_filepath
except Exception as e:
error_msg = f"画像生成エラー: {str(e)}"
return error_msg
def generate_story(self, current_situation, player_choice):
"""
GPT-4を使用してストーリーを生成する
Args:
current_situation (str): 現在のシーンの状況
player_choice (str): プレイヤーが選択した選択肢
Returns:
tuple: (ストーリー情報の辞書, 生成された画像のパス)
"""
# コンテキスト履歴を更新
self.context.append({
"situation": current_situation,
"choice": player_choice
})
context_history = self.context[-3:] # 最新3つのコンテキストのみ保持
# GPT-4へのプロンプトを設定
messages = [
{
"role": "system",
"content": "あなたは魅力的なビジュアルノベルを作るストーリーテラーです。JSON形式で次の展開と選択肢を提供してください。"
},
{
"role": "user",
"content": f"""
前回の状況: {json.dumps(context_history, ensure_ascii=False)}
現在の状況: {current_situation}
プレイヤーの選択: {player_choice}
以下のJSON形式で応答してください:
{{
"narrative": "次のシーンの描写、200文字以内",
"choices": ["選択肢1", "選択肢2", "選択肢3"],
"prompt": "次のシーンに基づくDALL-E 3画像生成プロンプト、英語で記述"
}}
"""
}
]
data = {
"model": "gpt-4o",
"messages": messages
}
# OpenAI APIにリクエストを送信
req = urllib.request.Request(
"https://api.openai.com/v1/chat/completions",
data=json.dumps(data).encode('utf-8'),
headers=self.headers,
method='POST'
)
try:
with urllib.request.urlopen(req, context=self.ssl_context) as response:
api_response = json.loads(response.read().decode('utf-8'))
content = api_response["choices"][0]["message"]["content"]
result = self._parse_response(content)
image = self.generate_image(result["prompt"])
return result, image
except Exception as e:
return {
"narrative": f"ストーリーの取得に失敗しました: {str(e)}",
"choices": ["再試行"],
}, None
def _parse_response(self, response):
"""
APIレスポンスからJSONデータを抽出して解析する
Args:
response (str): APIからのレスポンス文字列
Returns:
dict: パースされたJSONデータ、またはエラー情報
"""
try:
start = response.find('{')
end = response.rfind('}') + 1
json_str = response[start:end]
return json.loads(json_str)
except Exception as e:
return {
"narrative": f"申し訳ありません。一時的なエラーが発生しました: {str(e)}",
"choices": ["再試行"],
}, None
# StoryGeneratorのインスタンスを作成
story_generator = StoryGenerator()
# ナレーターキャラクターを定義
define narrator = Character('ナレーター', color="#c8ffc8")
# ゲーム開始
label start:
# 初期状態の設定
$ current_situation = "あなたは不思議な冒険を始めようとしています。"
$ player_choice = "冒険を始める"
# 最初の選択肢を表示
menu:
narrator "[current_situation]"
"[player_choice]":
pass
# メインゲームループ
while True:
# ストーリーと画像を生成
$ response, image = story_generator.generate_story(current_situation, player_choice)
# シーンの更新
scene black
if image:
show expression "[image]" at center
# ナレーションの表示
narrator "[response['narrative']]"
# 選択肢を事前に変数に保存
$ choices = response['choices']
# プレイヤーの選択肢を表示
menu:
"次の行動を選択してください"
"[choices[0]]":
$ player_choice = choices[0]
$ current_situation = response['narrative']
"[choices[1]]":
$ player_choice = choices[1]
$ current_situation = response['narrative']
"[choices[2]]":
$ player_choice = choices[2]
$ current_situation = response['narrative']