《如何用CrewAI让NPC活灵活现?惊爆游戏体验的秘密!》
- Rifx.Online
- Programming , Generative AI , Chatbots
- 19 Jan, 2025
大纲
- 使用 CrewAI 过滤对话并允许 NPC 选择任务
- CrewAI 代理
- 游戏代码和数据文件
- Github 仓库
- 结果/结论
背景
之前我在我的文章 使用智能体为NPC注入生命 中讨论了我对模拟二维社会的兴趣。有关更详细的背景,请阅读那篇文章。
在这篇后续文章中,我使用 CrewAI 来培养更好的响应,并增加每个个体根据其生物生成自己任务的能力。
这种自主性和对话质量的提高是朝着正确方向迈出的一大步,将有利于未来的对话分析。
船员 AI 代理
1. 对话代理
- 角色:
ConversationAgent
- 目标: 模拟人类般的交流,与用户进行有意义的对话。
- 背景和功能: 该代理使用对话历史和当前提示生成全面或总结的响应,允许动态和互动的交流,感觉自然且引人入胜。
2. 任务执行代理
- 角色:
OntaskAgent
- 目标: 保持专注并与个人任务保持一致。
- 背景和功能: 它使用最近的对话片段来保持讨论在预期主题范围内。当对话偏离时,它利用提示和有限的历史记录将其引导回用户任务的方向。
3. 完成任务代理
- 角色:
CompletedTaskAgent
- 目标: 确定并宣布任务导向活动的完成情况。
- 背景和功能: 使用短期历史回顾正在进行的任务。当任务完成时,它将其标记为已完成。这确保了任务状态的清晰沟通,在任务完成时结束相关操作。
4. 创建任务代理
- 角色:
CompletedTaskAgent
(角色名称相似但目的不同) - 目标: 报告正在进行的任务或在完成后生成新的任务。
- 背景故事和功能: 如果当前任务仍在进行中,则回应当前任务;否则,利用玩家的个人信息根据个人简历创建一个全新的任务。
5. 协作代理
- 角色:
OntaskAgent
(在此广泛角色下分类的另一个代理,用于协作) - 目标: 提供任务协助。
- 背景和功能: 突出支持能力,提供简洁的响应,以帮助任务进展。确保玩家在互动过程中有合作支持可用。
6. 唯一性代理
- 角色:
UniquenessAgent
- 目标: 最小化重复输出,提高响应多样性。
- 背景和功能: 该代理分析最近的对话历史,以检查重复性,并重新措辞内容,以创造独特的响应。这通过确保内容保持新鲜和有趣来维持用户参与度。
游戏代码和数据文件
数据文件:
- 世界由一个 CSV 文本文件定义
- 玩家名称 + 个人简介也存储在 CSV 中
游戏状态,包括世界设置和玩家数据,保存到 CSV 文件中。这确保了游戏进度可以被保存和重新加载。
这是我开始时的一个示例世界:
world.csv
grass,grass,grass,grass,rock,grass,grass,water,grass,grass
grass,grass,grass,grass,water,grass,rock,grass,water,grass
grass,water,grass,grass,grass,grass,grass,grass,water,water
water,grass,grass,grass,rock,grass,grass,grass,grass,grass
grass,grass,grass,grass,grass,grass,grass,grass,water,water
grass,grass,grass,grass,grass,water,water,grass,rock,water
grass,grass,water,grass,grass,grass,grass,rock,grass,grass
water,water,grass,water,grass,grass,grass,grass,grass,rock
water,grass,grass,grass,grass,grass,grass,water,water,water
water,grass,grass,grass,grass,water,grass,grass,rock,grass
player,Carly Cummings,4,1,1,100,100, Loves bacon, searching for bacon
player,Katherine Jones,3,0,100,100,100,Build a house,loves animals
player,Ashley Brown,1,7,100,100,100,finds nemo,loves fish
我们的初始游戏
- python Faker 库为我们的用户生成合成的名字和姓氏
- 作为玩家,Bio 和任务可以在 游戏暂停时 编辑。这将用于指导我们角色的对话
- 当游戏恢复时:一些通过偶然相遇的互动会根据个人的 Bio、任务和历史产生对话
简单游戏概述
简单游戏描述
游戏初始化:
- 加载资源(图像、世界数据)并初始化玩家。
- 设置初始位置并加载对话历史。
游戏循环:
- 事件处理(用户输入):
- 检测用户交互,例如键盘按键和鼠标点击。
- 点击暂停按钮时暂停或恢复游戏。
- 移动指令:上、下、左、右。
- 如果点击了玩家,则更新所选玩家。
2. 更新状态:
- 如果游戏未暂停:
- - 移动活动玩家。
- - 移动非活动玩家并检查附近的交互以开始对话。
- - 处理玩家交互,包括对发起的对话的响应。
- 如果游戏已暂停:
- - 点击玩家以查看/编辑并使其变为活动状态(这将使其在恢复时能够移动)
- - 更新活动用户的个人资料或任务(注意:可能会超出屏幕,但如果这是个问题,也可以更新CSV)
- - 使用上/下箭头滚动查看最近的对话
3. 渲染:
- 绘制游戏世界和玩家。
- 在暂停时从输入框更新玩家统计信息、个人资料和任务。
- 渲染UI元素,如聊天通知和暂停按钮。
玩家交互:
- 接近检测:当一个玩家接近另一个玩家时,可以发起对话。
- 对话处理:
- - 生成对话文本并使用OpenAI客户端请求响应。
- 保存和记录对话历史。
Python游戏需求文件
requirements.txt
pygame
faker
openai
Pygame 的图像资源:
- https://github.com/mtshomskyieee/agenticCity/tree/main/assets
对话派对 Python 游戏
conversation_party.py
import pygame
import random
import os
import csv
from faker import Faker
import openai
from openai import OpenAI
from crewai import Agent, Task, Crew, LLM, Process
### 本地设置 openai 密钥
## os.environ["OPENAI_API_KEY"] = "<YOUR KEY HERE>"
### 关闭对 'telemetry.crewai.com` 的遥测
os.environ["OTEL_SDK_DISABLED"] = "true"
## 初始化 Pygame 和 Faker
pygame.init()
fake = Faker()
## 屏幕尺寸和其他常量
GRID_SIZE = 10
CELL_SIZE = 40
WIDTH, HEIGHT = GRID_SIZE * CELL_SIZE * 2, GRID_SIZE * CELL_SIZE
FPS = 30
NUM_PLAYERS = 3 # 游戏中的玩家数量
SCROLL_SPEED = 5
RESPONSE_BOX_HEIGHT = 100
## 显示设置
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("对话派对")
clock = pygame.time.Clock()
WRAPPED_LINES = ""
## 跟踪聊天状态的全局变量
is_chatting = False
## 加载和调整图像大小的函数
def load_image(filename, size):
image = pygame.image.load(filename)
return pygame.transform.scale(image, size)
## 加载和调整图像大小
assets_dir = 'assets'
grass_img = load_image(os.path.join(assets_dir, 'grass.png'), (CELL_SIZE, CELL_SIZE))
rock_img = load_image(os.path.join(assets_dir, 'rock.png'), (CELL_SIZE, CELL_SIZE))
water_img = load_image(os.path.join(assets_dir, 'water.png'), (CELL_SIZE, CELL_SIZE))
player_imgs = [load_image(os.path.join(assets_dir, f'player_{i}.png'), (CELL_SIZE, CELL_SIZE)) for i in range(5)]
## 创建一个二维数组来生成世界或从 CSV 加载
def generate_world(grid_size):
world = [['grass' for _ in range(grid_size)] for _ in range(grid_size)]
for row in range(grid_size):
for col in range(grid_size):
rand = random.random()
if rand < 0.1:
world[row][col] = 'rock'
elif rand < 0.3:
world[row][col] = 'water'
return world
def load_world_from_csv(filename):
with open(filename, mode='r') as file:
reader = csv.reader(file)
return [row for row in reader]
def save_world_to_csv(filename, world, players):
with open(filename, mode='w', newline='') as file:
writer = csv.writer(file)
for row in world:
writer.writerow(row)
# 保存玩家的位置、名称、简介和任务
for player in players:
writer.writerow(['player', player.name, player.x, player.y, player.stats['Health'], player.stats['Speed'],
player.stats['Strength'], player.bio, player.tasks])
def generate_unique_position(existing_positions):
while True:
x, y = random.randint(0, GRID_SIZE - 1), random.randint(0, GRID_SIZE - 1)
if (x, y) not in existing_positions:
return x, y
conversation_dir = 'conversations'
os.makedirs(conversation_dir, exist_ok=True)
## 全局对话日志
global_conversation_log = []
def save_conversation_history(player):
filename = os.path.join(conversation_dir, f"{player.name.replace(' ', '_')}_conversation.txt")
with open(filename, 'w') as file:
file.write("\n".join(player.all_responses))
def load_conversation_history(player):
filename = os.path.join(conversation_dir, f"{player.name.replace(' ', '_')}_conversation.txt")
if os.path.exists(filename):
with open(filename, 'r') as file:
player.all_responses = file.read().splitlines()
def save_global_conversations():
filename = os.path.join(conversation_dir, 'global_conversation.txt')
with open(filename, 'w') as file:
for entry in global_conversation_log:
file.write(entry + "\n")
import openai
client = OpenAI(
# 这是默认值,可以省略
api_key=os.environ.get("OPENAI_API_KEY"),
)
def llm_request_crewai(
conversation_list,
request_string,
player_obj,
):
global is_chatting
is_chatting = True
# 在进行阻塞调用之前,更新显示以显示聊天状态。
draw_chatting_notification(screen)
pygame.display.flip()
# 将对话历史与请求字符串结合起来。
conversation_history = "\n".join(conversation_list)
# 构造完整提示
prompt = f"\n{request_string}"
messages = [
{"role": "user", "content": conversation_history},
{"role": "system", "content": "你是一个与人类对话的角色。"},
{"role": "user", "content": prompt}
]
tasks = str(player_obj.tasks)
my_bio = str(player_obj.bio)
# 将 CrewAI 作为 llm_request 的替代品,后者是之前的 LLM 接口函数
#
# 不可否认的是,下面的 Agents 和 Tasks 应该移出函数定义
# 在寻求保持简单的情况下,专注于功能
# 知道过早优化是万恶之源。 预计以下内容将在未来版本中移出或进一步抽象。
llm = LLM(model="gpt-4o", temperature=0.7, api_key=os.environ["OPENAI_API_KEY"])
conversation_agent = Agent(
role="ConversationAgent",
goal=f"我是一名希望与他人对话的人",
backstory=f"{prompt} 在过去,我讨论过 {conversation_history[:100]}",
llm=llm,
memory=True,
verbose=True,
)
conversation_task = Task(
description="开始玩家对话",
expected_output="如果需要,提供详细的回复;如果需要,使用用户的历史提供摘要回复。",
agent=conversation_agent,
)
ontask_agent = Agent(
role="OntaskAgent",
goal=f"保持在任务上,{prompt}",
backstory=f"{prompt}, 在最近的过去,我讨论过 {conversation_history[:2]},如果我没有在任务上,"
f"请讨论。",
llm=llm,
memory=True,
verbose=True,
)
ontask_task = Task(
description="讨论与任务相关的内容",
expected_output="使用用户的任务和迄今为止的对话提供回复。",
agent=ontask_agent,
)
completedtask_agent = Agent(
role="CompletedTaskAgent",
goal=f"如果我的任务完成了 {tasks},请说已完成",
backstory=f"{prompt}, 在最近的过去,我讨论过 {conversation_history[:6]}。 如果我完成了我的 "
f"任务,请将任务标记为已完成。",
llm=llm,
memory=True,
verbose=True,
)
completedtask_task = Task(
description="标记任务为完成",
expected_output=f"我已完成我的任务 {tasks}",
agent=completedtask_agent,
)
createtask_agent = Agent(
role="CompletedTaskAgent",
goal=f"如果我尚未完成我的任务 {tasks},请回复我的任务;否则,"
f"如果我的任务完成了 {tasks},请生成一个新任务。",
backstory=f"{prompt}. 如果我完成了 "
f"任务,请根据 {my_bio} 创建一个新的 5 字任务。",
llm=llm,
memory=True,
verbose=True,
)
createtask_task = Task(
description="生成新任务",
expected_output=f"我已完成我的任务 {tasks},生成一个新的任务",
agent=createtask_agent,
)
collaborate_agent = Agent(
role="OntaskAgent",
goal=f"帮助完成任务",
backstory=f"{prompt}, 我能够帮助任何任务。",
llm=llm,
memory=True,
verbose=True,
)
collaborate_task = Task(
description="帮助完成任务",
expected_output="简短的回复以帮助完成任务。",
agent=collaborate_agent,
)
uniqueness_agent = Agent(
role="UniquenessAgent",
goal=f"我需要在回复中减少重复性",
backstory=f"{prompt}, 如果我发现自己重复以下内容 {conversation_history[:100]},请改述回复",
llm=llm,
memory=True,
verbose=True,
)
uniqueness_task = Task(
description="开始玩家对话",
expected_output="使用用户的历史提供独特的回复。",
agent=uniqueness_agent,
)
try:
## 对话
crew = Crew(
agents=[ontask_agent, collaborate_agent, uniqueness_agent, conversation_agent],
tasks=[ontask_task, collaborate_task, uniqueness_task, conversation_task],
process=Process.sequential,
verbose=True
)
result = crew.kickoff(inputs={
'prompt': prompt,
'history': conversation_history,
'messages': messages,
'tasks': str(player_obj.tasks),
'my_bio': str(player_obj.bio),
})
## 任务反思
taskCrew = Crew(
agents=[completedtask_agent, createtask_agent],
tasks=[completedtask_task, createtask_task],
process=Process.sequential,
verbose=True
)
taskResult = taskCrew.kickoff(inputs={
'prompt': prompt,
'history': conversation_history,
'messages': messages,
'tasks': str(player_obj.tasks),
'my_bio': str(player_obj.bio),
})
# 让我们接受任务重新分配的概率为 30%
if random.random() < 0.3:
old_task = str(player_obj.tasks)
player_obj.tasks = str(taskResult)
global_conversation_log.append(f"{player_obj.name} (更改任务): 完成({old_task}), 开始({player_obj.tasks})")
is_chatting = False
return str(result.raw)
except Exception as e:
is_chatting = False
return f"发生错误: {str(e)}"
def wrap_text(text, font, max_width):
"""将文本换行以适应指定宽度。"""
words = text.split(' ')
wrapped_lines = []
current_line = ""
for word in words:
# 检查如果我们添加这个单词,新行的宽度
if font.size(current_line + word)[0] <= max_width:
current_line += word + " "
else:
wrapped_lines.append(current_line)
current_line = word + " "
if current_line:
wrapped_lines.append(current_line)
return wrapped_lines
## 处理暂停/恢复按钮的按钮类
class Button:
def __init__(self, x, y, w, h, text):
self.rect = pygame.Rect(x, y, w, h)
self.text = text
self.color = (173, 216, 230) # 浅蓝色
self.outline_color = (0, 0, 0) # 黑色轮廓
self.font = pygame.font.Font(None, 36)
def draw(self, screen):
# 绘制轮廓
pygame.draw.rect(screen, self.outline_color, self.rect, 2) # 2 像素边框
# 绘制按钮内部
pygame.draw.rect(screen, self.color, self.rect.inflate(-4, -4))
# 渲染文本
text_surf = self.font.render(self.text, True, (0, 0, 0))
text_rect = text_surf.get_rect(center=self.rect.center)
screen.blit(text_surf, text_rect)
def is_clicked(self, event):
return event.type == pygame.MOUSEBUTTONDOWN and self.rect.collidepoint(event.pos)
## 按钮设置
button_width, button_height = 120, 40
pause_button = Button(WIDTH - button_width - 10, HEIGHT - button_height - 10, button_width, button_height, "暂停")
class Player:
def __init__(self, x, y, image):
self.x = x
self.y = y
self.image = image
self.name = fake.name()
self.stats = {'Health': 100, 'Speed': 5, 'Strength': 10}
self.bio = fake.text(max_nb_chars=100)
self.tasks = fake.text(max_nb_chars=100)
self.current_conversation = ""
self.all_responses = []
self.health = 100
self.speed = 100
self.strength = 100
self.current_scroll = 0
load_conversation_history(self)
def who_am_i_string(self):
return f"我叫 {self.name}。我的背景是: {self.bio}。我目前的任务是 {self.tasks}。"
def generate_conversation(self):
self.current_conversation = self.who_am_i_string()
response_string = llm_request_crewai(self.all_responses, self.current_conversation, self)
self.all_responses.append(response_string)
return response_string
def respond_conversation(self, text, other_player_name):
self.current_conversation = self.who_am_i_string()
self.current_conversation += f"我正在与一个人交谈,他说: {text}"
self.current_conversation += "用一句话,我应该如何回应"
response_string = llm_request_crewai(self.all_responses, self.current_conversation, self)
self.all_responses.append(response_string)
print(f"{self.name} 对话:{self.current_conversation}")
print(f"{self.name} 回复:{response_string}")
# 添加到全局对话日志
global_conversation_log.append(f"{other_player_name} (与 {self.name} 交谈): {text}")
global_conversation_log.append(f"{self.name} (与 {other_player_name} 交谈): {response_string}")
save_conversation_history(self)
return response_string
def find_nearby_player(player, players):
for other_player in players:
if other_player != player:
if abs(player.x - other_player.x) <= 1 and abs(player.y - other_player.y) <= 1:
return other_player
return None
## 检查 world.csv 是否存在
world_filename = 'world.csv'
if os.path.exists(world_filename):
data = load_world_from_csv(world_filename)
world = [row for row in data if row[0] not in ['player']]
players_data = [row for row in data if row[0] == 'player']
players = []
for player_data in players_data:
_, name, x, y, health, speed, strength, bio, tasks = player_data
player = Player(int(x), int(y), player_imgs[len(players)])
player.name = name
player.stats = {'Health': int(health), 'Speed': int(speed), 'Strength': int(strength)}
player.bio = bio
player.tasks = tasks
load_conversation_history(player)
players.append(player)
else:
world = generate_world(GRID_SIZE)
# 用唯一位置初始化玩家
players = []
existing_positions = set()
for i in range(NUM_PLAYERS):
x, y = generate_unique_position(existing_positions)
existing_positions.add((x, y))
players.append(Player(x, y, player_imgs[i]))
save_world_to_csv(world_filename, world, players)
active_player_idx = random.randint(0, NUM_PLAYERS - 1)
def move_player(player, dx, dy):
new_x, new_y = player.x + dx, player.y + dy
if 0 <= new_x < GRID_SIZE and 0 <= new_y < GRID_SIZE and world[new_x][new_y] != 'rock':
# 检查新位置是否被其他玩家占用
if not any(p.x == new_x and p.y == new_y for p in players):
player.x, player.y = new_x, new_y
def move_away(player, other_player):
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
random.shuffle(directions)
for dx, dy in directions:
new_x, new_y = player.x + dx, player.y + dy
if 0 <= new_x < GRID_SIZE and 0 <= new_y < GRID_SIZE and world[new_x][new_y] != 'rock':
if not any(p.x == new_x and p.y == new_y for p in players):
player.x, player.y = new_x, new_y
break
def move_inactive_players(players, active_idx):
for i, player in enumerate(players):
if i != active_idx:
nearby_player = find_nearby_player(player, players)
if nearby_player:
conversation = player.generate_conversation()
response = nearby_player.respond_conversation(conversation, player.name)
print(f"{player.name}: 对话 {conversation}")
print(f"{nearby_player.name}: 回复 {response}")
player.all_responses.append(response)
move_away(player, nearby_player)
move_away(nearby_player, player)
else:
dx, dy = random.choice([(0, 1), (1, 0), (0, -1), (-1, 0)])
move_player(player, dx, dy)
def draw_world(surface, world, players):
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
image = grass_img if world[row][col] == 'grass' else rock_img if world[row][col] == 'rock' else water_img
surface.blit(image, (col * CELL_SIZE, row * CELL_SIZE))
for player in players:
surface.blit(player.image, (player.y * CELL_SIZE, player.x * CELL_SIZE))
## 更新 draw_stats 以显示最近的回复
def draw_stats(surface, player, x_offset, y_offset):
global WRAPPED_LINES # 用于 UI 计算
font = pygame.font.Font(None, 36)
y = y_offset
# 显示玩家名称
name_text = font.render(player.name, True, (0, 0, 0))
surface.blit(name_text, (x_offset, y))
y += 40
# Bio 和 Tasks 的静态位置
bio_label_y = y
tasks_label_y = y + 40
# 渲染标签
bio_label = font.render('简介:', True, (0, 0, 0))
tasks_label = font.render('任务:', True, (0, 0, 0))
# 在同一部分显示简介和任务
surface.blit(bio_label, (x_offset, bio_label_y))
surface.blit(tasks_label, (x_offset, tasks_label_y))
# 显示最近的回复
if player.all_responses:
y+=80
wrapped_lines = wrap_text('最近: ' + player.all_responses[-1], font, WIDTH - x_offset - 20)
WRAPPED_LINES = wrapped_lines
# 滚动调试
# print(f"{player.current_scroll}, {len(wrapped_lines)} : {player.current_scroll + (RESPONSE_BOX_HEIGHT // 20)} : {WRAPPED_LINES},")
for line in WRAPPED_LINES[player.current_scroll: player.current_scroll + (RESPONSE_BOX_HEIGHT // 20)]:
surface.blit(font.render(line, True, (0, 0, 0)), (x_offset, y))
y += 20
# 滚动指示器(可选)
if len(wrapped_lines) > (RESPONSE_BOX_HEIGHT // 20) and paused:
surface.blit(font.render("[上/下]:滚动", True, (155, 155, 155)), (x_offset, y+10))
# 在暂停模式下绘制输入框
if paused:
bio_box.rect.topleft = (x_offset + 80, bio_label_y)
tasks_box.rect.topleft = (x_offset + 80, tasks_label_y)
bio_box.draw(screen)
tasks_box.draw(screen)
else:
# 未暂停,显示当前简介和任务
bio_text = font.render(player.bio, True, (0, 0, 0))
tasks_text = font.render(player.tasks, True, (0, 0, 0))
surface.blit(bio_text, (x_offset + 80, bio_label_y))
surface.blit(tasks_text, (x_offset + 80, tasks_label_y))
## 绘制 "聊天中..." 通知的函数
def draw_chatting_notification(surface):
notification_rect = pygame.Rect(WIDTH - 300, HEIGHT - 50, 150, 40)
pygame.draw.rect(surface, (200, 200, 255), notification_rect) # 白色框
pygame.draw.rect(surface, (0, 0, 0), notification_rect, 2) # 黑色边框
font = pygame.font.Font(None, 30)
text_surf = font.render("聊天中...", True, (0, 0, 0))
surface.blit(text_surf, (notification_rect.x + 10, notification_rect.y + 10))
class InputBox:
def __init__(self, x, y, w, h, text=''):
self.rect = pygame.Rect(x, y, w, h)
self.color_active = (173, 216, 230) # 浅蓝色
self.color_inactive = (0, 0, 0) # 黑色
self.text = text
self.txt_surface = pygame.font.Font(None, 36).render(text, True, self.color_inactive)
self.active = False
self.enter_pressed = False # 跟踪 Enter 是否被按下
def handle_event(self, event, paused):
if event.type == pygame.MOUSEBUTTONDOWN:
# 切换活动变量。
if self.rect.collidepoint(event.pos):
if paused: # 仅在游戏暂停时激活
self.active = not self.active
else:
self.active = False
if event.type == pygame.KEYDOWN:
if self.active:
if event.key == pygame.K_RETURN:
self.enter_pressed = True # 标记 Enter 被按下
elif event.key == pygame.K_BACKSPACE:
self.text = self.text[:-1]
else:
self.text += event.unicode
# 重新渲染文本。
self.txt_surface = pygame.font.Font(None, 36).render(self.text, True, self.color_active if self.active else self.color_inactive)
def draw(self, screen):
# 绘制文本。
screen.blit(self.txt_surface, (self.rect.x + 5, self.rect.y + 5))
# 绘制矩形。
pygame.draw.rect(screen, self.color_active if self.active else self.color_inactive, self.rect, 2)
def update_text(self, text):
self.text = text
self.txt_surface = pygame.font.Font(None, 36).render(text, True,
self.color_active if self.active else self.color_inactive)
def reset_enter_pressed(self):
self.enter_pressed = False
def get_player_at_pos(players, x, y):
for i, player in enumerate(players):
player_rect = pygame.Rect(player.y * CELL_SIZE, player.x * CELL_SIZE, CELL_SIZE, CELL_SIZE)
if player_rect.collidepoint(x, y):
return i
return None
## 初始配置
sub_epoch = 30 # 时间增量
idle_mod = 20 % sub_epoch # 闲置事物的更新周期
idle_count = 0
running = True
paused = False
selected_player = players[active_player_idx]
## 为玩家属性创建输入框
stat_boxes = [
InputBox(GRID_SIZE * CELL_SIZE + 100, 100 + i * 40, 100, 32, str(getattr(selected_player, stat.lower())))
for i, stat in enumerate(['Health', 'Speed', 'Strength'])
]
## 260, 350
bio_box = InputBox(GRID_SIZE * CELL_SIZE + 20, 20, 300, 32, selected_player.bio)
tasks_box = InputBox(GRID_SIZE * CELL_SIZE + 20, 50, 300, 32, selected_player.tasks)
#input_boxes = stat_boxes + [bio_box, tasks_box]
input_boxes = [bio_box, tasks_box]
while running:
idle_count = (idle_count + 1) % sub_epoch
for event in pygame.event.get():
if pause_button.is_clicked(event):
paused = not paused
pause_button.text = "恢复" if paused else "暂停"
if event.type == pygame.QUIT:
save_world_to_csv(world_filename, world, players)
for player in players:
save_conversation_history(player)
save_global_conversations()
running = False
# elif event.type == pygame.KEYDOWN:
# if event.key == pygame.K_p and not any(box.enter_pressed for box in input_boxes):
# paused = not paused
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_x, mouse_y = event.pos
clicked_player_idx = get_player_at_pos(players, mouse_x, mouse_y)
if clicked_player_idx is not None:
active_player_idx = clicked_player_idx
selected_player = players[active_player_idx]
for stat, box in zip(selected_player.stats.values(), stat_boxes):
box.update_text(str(stat))
bio_box.update_text(selected_player.bio)
tasks_box.update_text(selected_player.tasks)
# 处理输入框,即使在暂停时
for box in input_boxes:
box.handle_event(event, paused)
# 仅在未暂停时处理其他游戏逻辑
if not paused:
keys = pygame.key.get_pressed()
if keys[pygame.K_UP]:
move_player(players[active_player_idx], -1, 0)
if keys[pygame.K_DOWN]:
move_player(players[active_player_idx], 1, 0)
if keys[pygame.K_LEFT]:
move_player(players[active_player_idx], 0, -1)
if keys[pygame.K_RIGHT]:
move_player(players[active_player_idx], 0, 1)
if idle_count % idle_mod == 0:
move_inactive_players(players, active_player_idx)
else: # 暂停时,允许滚动
keys = pygame.key.get_pressed()
if keys[pygame.K_UP]:
if selected_player.current_scroll > 0:
selected_player.current_scroll -= SCROLL_SPEED
elif keys[pygame.K_DOWN]:
if selected_player.current_scroll < len(WRAPPED_LINES) - (RESPONSE_BOX_HEIGHT // 20):
selected_player.current_scroll += SCROLL_SPEED
screen.fill((255, 255, 255))
draw_world(screen, world, players)
for stat, box in zip(selected_player.stats.keys(), stat_boxes):
try:
value = int(box.text)
selected_player.stats[stat] = value
except ValueError:
pass
selected_player.bio = bio_box.text
selected_player.tasks = tasks_box.text
draw_stats(screen, selected_player, GRID_SIZE * CELL_SIZE + 20, 10)
# 如果暂停,允许输入更新
if paused:
for box in input_boxes:
box.draw(screen)
# 绘制暂停/恢复按钮
pause_button.draw(screen)
# 如果启用聊天,绘制聊天通知
if is_chatting:
draw_chatting_notification(screen)
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
结果/结论
- 代理帮助集中对话,并允许个人选择不同的任务。
- 初始角色设定为“喜欢动物”、“喜欢鱼”和“寻找培根”等个性描述,缺乏应有的深度,这应该会导致更丰富的互动。
- 在当前设置下,我目睹了许多对话围绕各自的任务展开,并真正尝试保持对话的集中性。
下一步
- 与不同人群的对话分析
- 增加玩家的深度,并允许他们与环境互动
Github Repo
如果您觉得这篇文章很有见地,请考虑为这篇文章点赞——这不仅支持作者,还帮助其他人发现有价值的见解。此外,不要忘记订阅以获取更多深入探讨创新技术和人工智能发展的文章。感谢您的参与!