Type something to search...
《如何用CrewAI让NPC活灵活现?惊爆游戏体验的秘密!》

《如何用CrewAI让NPC活灵活现?惊爆游戏体验的秘密!》

大纲

  • 使用 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、任务和历史产生对话

简单游戏概述

简单游戏描述

游戏初始化:

  • 加载资源(图像、世界数据)并初始化玩家。
  • 设置初始位置并加载对话历史。

游戏循环:

  1. 事件处理(用户输入):
  • 检测用户交互,例如键盘按键和鼠标点击。
  • 点击暂停按钮时暂停或恢复游戏。
  • 移动指令:上、下、左、右。
  • 如果点击了玩家,则更新所选玩家。

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

如果您觉得这篇文章很有见地,请考虑为这篇文章点赞——这不仅支持作者,还帮助其他人发现有价值的见解。此外,不要忘记订阅以获取更多深入探讨创新技术和人工智能发展的文章。感谢您的参与!

Related Posts

使用 ChatGPT 搜索网络功能的 10 种创意方法

使用 ChatGPT 搜索网络功能的 10 种创意方法

例如,提示和输出 你知道可以使用 ChatGPT 的“搜索网络”功能来完成许多任务,而不仅仅是基本的网络搜索吗? 对于那些不知道的人,ChatGPT 新的“搜索网络”功能提供实时信息。 截至撰写此帖时,该功能仅对使用 ChatGPT 4o 和 4o-mini 的付费会员开放。 ![](https://images.weserv.nl/?url=https://cdn-im

阅读更多
在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和科技这样一个动态的行业中,保持领先意味着不断提升你的技能。无论你是希望深入了解人工智能模型性能、掌握数据分析,还是希望通过人工智能转变传统领域如法律,这些课程都是你成功的捷径。以下是一个精心策划的高价值课程列表,可以助力你的职业发展,并让你始终处于创新的前沿。 1. 生成性人工智能简介课程: [生成性人工智能简介](https://genai.works

阅读更多
10 个强大的 Perplexity AI 提示,让您的营销任务自动化

10 个强大的 Perplexity AI 提示,让您的营销任务自动化

在当今快速变化的数字世界中,营销人员总是在寻找更智能的方法来简化他们的工作。想象一下,有一个个人助理可以为您创建受众档案,建议营销策略,甚至为您撰写广告文案。这听起来像是一个梦想? 多亏了像 Perplexity 这样的 AI 工具,这个梦想现在成为现实。通过正确的提示,您可以将 AI 转变为您的 个人营销助理。在本文中,我将分享 10 个强大的提示,帮助您自动

阅读更多
10+ 面向 UI/UX 设计师的顶级 ChatGPT 提示

10+ 面向 UI/UX 设计师的顶级 ChatGPT 提示

人工智能技术,如机器学习、自然语言处理和数据分析,正在重新定义传统设计方法。从自动化重复任务到实现个性化用户体验,人工智能使设计师能够更加专注于战略思维和创造力。随着这一趋势的不断增长,UI/UX 设计师越来越多地采用 AI 驱动的工具来促进他们的工作。利用人工智能不仅能提供基于数据的洞察,还为满足多样化用户需求的创新设计解决方案开辟了机会。 1. 用户角色开发 目的

阅读更多
在几分钟内完成数月工作的 100 种人工智能工具

在几分钟内完成数月工作的 100 种人工智能工具

人工智能(AI)的快速发展改变了企业的运作方式,使人们能够在短短几分钟内完成曾经需要几周或几个月的任务。从内容创作到网站设计,AI工具帮助专业人士节省时间,提高生产力,专注于创造力。以下是按功能分类的100个AI工具的全面列表,以及它们在现实世界中的使用实例。 1. 研究工具 研究可能耗时,但人工智能工具使查找、分析和组织数据变得更加容易。**ChatGPT, Cop

阅读更多
你从未知道的 17 个令人惊叹的 GitHub 仓库

你从未知道的 17 个令人惊叹的 GitHub 仓库

Github 隐藏的宝石!! 立即收藏的代码库 学习编程相对简单,但掌握编写更好代码的艺术要困难得多。GitHub 是开发者的宝藏,那里“金子”是其他人分享的精心编写的代码。通过探索 GitHub,您可以发现如何编写更清晰的代码,理解高质量代码的样子,并学习成为更熟练开发者的基本步骤。 1. notwaldorf/emoji-translate *谁需

阅读更多