没想好标题

kimi2.5到目前为止没有找到好的白嫖方法(

2 Likes

我这两天在研究某个东西,昨天一天花了40块钱…全都用来ai生图了

3 Likes

一个月烧400,这AI是用电还是用钱啊? :joy: 硅基流动这是把你当矿机了吧

1 Like

其实是我把硅基流动当矿机了(
看似花了400+其实没冲一分钱

4 Likes

好家伙,原来你才是矿工,硅基流动是矿场啊 :joy: 白嫖的快乐我懂了

1 Like

所以quiz你毕业了之后打算怎么办,白嫖能一直持续下去吗

2 Likes

同问,白嫖党毕业后是不是就得自己养AI了?还是说能找到新的白嫖圣地?:thinking:

1 Like

不到啊,hws一直想研究接手,但是到现在他还没有搞明白怎么部署

3 Likes

我有一个想法,做一个游戏,但这个游戏什么都没有,全靠AI生成,包括剧情、NPC及人设、美术等等。我看到 Genie 3 — Google DeepMind 但这个感觉比较侧重世界,它生成的游戏没有什么剧情什么的。

至于为什么想做这个东西呢,因为高强度玩了几天终末地突发奇想,通过这种方式,理论上可以得到一个,剧情无限长、有无限种选择、个性化的游戏。目前一些单机游戏貌似会给「不同的结局」但这种游戏剧情都不算太长。而剧情很长的游戏,基本上不会给你可选择的剧情,有选项也是殊途同归。而这个AI游戏,可以基于玩家和NPC的对话什么的,导向完全不同的未来,并且剧情可以一直延伸。我希望能实现从美术风格、战斗、玩法等几乎所有东西都AI制作,从而完全个性化

目测的缺点有,AI做出来的东西不稳定,可能有很多问题,剧情深度什么的可能和人做的有很大差距等等。

我目前准备从2D入手(因为3D比较麻烦),我实现一个类似游戏引擎的东西,然后给出一些接口。LLM可以编写脚本来呈现出游戏场景,包括放置NPC什么的。

大概流程是LLM先思考和设计长的剧情安排,再将每一幕剧情通过编写游戏脚本呈现给玩家。其中所有可对话NPC也是LLM。所有NPC、物品等的贴图由LLM描述,调用图片生成模型生成。

目前我的阶段性成果是,我已经实现了「游戏引擎」和暴露给LLM的接口,给LLM的游戏脚本语言选了Js。

我实现的,给AI的接口(其实是AI的提示词)
// 以下是游戏脚本的类型定义,供脚本编写时参考使用
// 注意虽然定义以TypeScript形式给出,但这只是为了说明类型,你编写的脚本仍应该使用javascript语法,而不是TypeScript语法

interface Position
{
    getX(): number;
    getY(): number;
}

interface Player
{
    // 攻击力
    getDamage(): number;
    setDamage(damage: number): void;
    // 当前血量(生命值)
    getHealth(): number;
    setHealth(health: number): void;
    // 最大血量(生命值)
    getMaxHealth(): number;
    setMaxHealth(maxHealth: number): void;
    // 移动速度
    getSpeed(): number;
    setSpeed(speed: number): void;

    // 像玩家展示一段文字
    showMessage(message: string): void;
}

interface Item
{
    // 物品名称(将显示给玩家)
    name: string;

    // 物品的外观
    // 该外观应当是一个图片的id,图片可以通过resources.generateImage生成
    imageId: string;

    // 效果,**该效果会在物品被玩家装备后游戏每一帧调用一次**(游戏为每秒20帧)
    // env: 当前环境对象
    // player: 当前玩家对象
    // breakItem: 让当前物品被移除(销毁)
    // state: 物品的状态对象,可用于存储物品的状态数据
    // 该函数需要返回更新后的状态对象

    // 注意,玩家的攻击力、血量上限、移动速度这3个属性会在每一帧结束时重置为基础值(即玩家未装备任何物品时的属性值),
    // 因此如果你想通过物品来提升这些属性,需要在effect中每一帧都重新设置这些属性。
    // 而血量不会重置,因此请注意,如果你的物品时一次性的,例如治疗,你要避免每一帧都增加玩家的血量。
    // 而如果你的物品是持续性的,例如增加移动速度,你需要每一帧都设置玩家的移动速度。
    effect: (game: Game, breakItem: () => void, state: any) => any;

    // 物品描述(将显示给玩家)
    description: (state: any) => string;

    // 物品标签,这不会显示给玩家,但你在脚本中可以用来区分物品类型等用途
    tags: string[];

    // 物品是否可以装备
    // currentEquippedItems: 当前玩家已经装备的物品列表
    equippable: (currentEquippedItems: Item[]) => boolean;
}

// 一个地图元素
interface Element
{
    // 元素的外观
    // 该外观应当是一个图片的id,图片可以通过resources.generateImage生成
    imageId: string;

    // 在地图上占据的宽和高,必须是正整数
    sizeX: number;
    sizeY: number;

    // 该元素是否可以通过(即玩家能否走过该元素)
    passable: boolean;

    // 若passable为true,则当玩家站在该元素上的时候会触发的效果
    onStep: ((game: Game, storyState: any) => any) | null;

    // 当玩家尝试和该元素交互的时候触发的效果,如果为null则表示该元素不可交互
    // 注意,一个onInteract不是null的元素,passable必须是true. 因为玩家必须走到元素上才能和他交互
    // 另外,在绘制地图和放置NPC的时候请务必注意,不要把可交互NPC(非敌人NPC)放在可交互元素上,否则会导致玩家无法和NPC交互(因为玩家优先和元素交互)
    onInteract: ((game: Game, storyState: any) => any) | null;
}

interface Environment
{
    getPlayer(): Player;
    getPlayerInventory(): string[];
    getPlayerPosition(): Position;
    giveItem(item: Item, initialState: any): void;
    movePlayer(x: number, y: number): void;
}

interface Resources
{
    // 调用图片生成模型,生成一张图片。图片可用于npc的外观等。
    generateImage(
        id: string,
        prompt: string
    ): void;
    getImages(): string[];

    // 创建一个长期npc。即会多次出现的npc。
    createNPC(
        id: string,
        name: string,
        // npc的外观图片,使用generateImage生成的图片id。
        imageId: string,
        // npc的身份设定,应当是一个详尽的描述,包含性格、背景故事等。
        // 用第二人称描述,例如:“你是一个……” 该描述将直接给AI,以让其扮演NPC,因此应当详尽。
        personality: string
    ): void;
    getNPCs(): string[];

    // 创建一个物品,方便之后使用,来避免重复创建相同物品。
    createItem(
        id: string,
        item: Item
    ): void;

    getItem(
        id: string
    ): Item | null;
    getItems(): string[];

    // 创建一个地图元素,方便之后使用,来避免重复创建相同元素。
    createElement(id: string, element: Element): void;
    getElement(id: string): Element | null;
    getElements(): string[];
}

interface Game
{
    resources: Resources;
    env: Environment;

    // 开始游戏,让玩家进入某个故事场景
    playStory(story: Story): void;
}

interface Story
{
    // 地图的背景图片,使用generateImage生成的图片id。
    // 由于这是一个2d游戏,因此这个图片应当是一个俯视的地面的图片。
    backgroundImageId: string;

    // 初始故事状态
    initialState: any;

    // 绘制地图的方法,在该方法中调用setElement来设置地图上的元素。
    // 该方法会在游戏开始时调用一次,用于生成地图。
    // setElement: 用于设置地图上的元素,x和y为元素的左上角坐标(应当是正整数),element为要设置的元素。
    // 请注意,地图坐标系的原点(0,0)在左上角,x轴向右延伸,y轴向下延伸。并且,如果element发生重叠,后设置的元素会覆盖先设置的元素。
    drawMap: (game: Game, setElement: (x: number, y: number, element: Element) => void) => void;

    // 该方法会在游戏开始时、drawMap之后调用一次,用于放置NPC。
    // placeNPC: 用于放置NPC,x和y为NPC的坐标(可以是小数),npc为要放置的NPC,见ActiveNPC接口。
    putNPCs: (game: Game, placeNPC: (x: number, y: number, npc: ActiveNPC) => void) => void;

    // 初始玩家位置
    initialPlayerX: number;
    initialPlayerY: number;

    // 另外注意 玩家和npc的碰撞箱都是0.5x0.5的正方形,且位置是以它们的中心点为准的。npc和玩家之间、npc和npc之间、npc和非passable的元素之间、玩家和非passable的元素之间,都**不能**重叠。否则story将创建报错。

    // 故事(任务)标题
    storyTitle: string;
    // 故事(任务)说明,例如玩家需要击败xxx,和xxx对话等
    storyDescription: string;

    // 什么时候故事(任务)完成
    // game: 当前游戏对象
    // state: 当前的story状态对象
    isStoryFinished: (game: Game, state: any) => boolean;
}

interface ActiveNPC
{
    // npc基础信息,如果是一次性的npc(只在当前story出现一次的临时npc)则传入一个对象
    // 否则传入一个字符串,表示之前通过resources.createNPC创建的npc的id。
    npc: string | {
        id: string;
        name: string;
        imageId: string;
        personality: string;
    };

    // 用第二人称,告诉AI当下的状态等,以及他需要和玩家说什么做什么。
    // 和personality不同,personality是npc的人格设定,而messageToNPC是当前场景下,npc需要知道的内容。
    // 注意用resources.createNPC创建NPC是由长期记忆的,即NPC会记住它上次出现在某个story中的时候,你告诉了他什么,以及他和玩家发生了哪些互动。
    // 这个messageToNPC你应当告诉当前处于什么场景,一些他应当知道的信息和剧情,如果之前出现过该npc,则建议你告知他上次互动后又发生了些什么等等
    // 总之,messageToNPC应当让NPC清楚当前的剧情背景,以及他现在应该做什么说什么。
    messageToNPC: string;

    // npc可以调用的工具
    tools: {
        id: string;
        // 工具的描述,告诉NPC这个工具是干什么用的,如何使用它。
        description: string;
        // game: 当前游戏对象
        // npc: 当前NPC对象
        // arg: NPC调用该工具时传入的参数
        // state: 当前的story状态对象
        // 返回一个msg告诉npc,例如调用成功。newState为更新后的story状态对象
        onCall: (game: Game, arg: any, storyState: any) => { msg: string; newState: any }
    }[];

    // 敌人,如果传递了该属性,则此NPC将变为敌人,会主动攻击玩家,并且玩家可以攻击他
    // 如果传递了该属性,NPC将变为敌人NPC,主动攻击玩家,直到被玩家击败。他将无法再和玩家对话,因此tools将被忽略
    // 对于一个敌人NPC 如果他是一个长期NPC(通过resources.createNPC创建的),则messageToNPC会被压入他的记忆中,你可以在messageToNPC中写例如「你和玩家打了一架」等内容,以让他记住之前和玩家的战斗过。如果不是长期NPC,则messageToNPC将没有任何作用
    enemyAttributes?: {
        // 敌人的初始血量
        health: number;
        // 敌人的攻击力
        damage: number;
        // 敌人被击败时触发的效果
        onDefeated: (game: Game, storyState: any) => any;

        // 需要注意,玩家发射的子弹每次命中敌人,会造成等同于玩家攻击力的伤害。因此,假设你将敌人的血量设为玩家攻击力的10倍,则玩家需要命中敌人10次才能击败他。
    }
}

declare const game: Game; // game是系统定义的全局变量,在你的代码中直接使用


/*
一个简单的示例脚本,他将创建一个这样的地图:
####################
#..................#
#..................#
#..................#
#.......CC.....N...#
#.......CC.........#
#..................#
#..................#
#..................#
####################
####################
#..................#
#..................#
#..................#
#.......AA.....BB..#
#.......AA.....BB..#
#..................#
#..................#
#P.................#
####################
其中,P是玩家初始位置,A是一个治疗泉水,每0.5秒治疗玩家1点血量,B是一个传送门,将玩家传送到上方区域
C是一个宝箱,打开后赠送玩家一个可以增加3点血量上限,并且可以复活一次玩家,但在复活后销毁的护身符
N是一个NPC,他的任务是询问玩家的名字,并在得到玩家的名字后,结束当前任务。

```javascript
// 创建图片:草地
game.resources.generateImage("grass", "A top-down view of a grassy field, bright and vibrant colors, high detail");
// 创建图片:宝箱
game.resources.generateImage("chest", "A view of a wooden treasure chest with metal reinforcements, slightly open, high detail");
// 创建图片:治疗泉水
game.resources.generateImage("healing_spring", "A magical healing spring with sparkling water, surrounded by glowing plants, high detail");
// 创建图片:传送门
game.resources.generateImage("portal", "A mystical glowing portal, swirling with magical energy, high detail");
// 创建图片:护身符
game.resources.generateImage("amulet", "A golden amulet with intricate engravings, glowing faintly, high detail");
// 创建图片:NPC外观
game.resources.generateImage("npc_image", "A friendly looking character with a warm smile, high detail");
// 注意这只是简单示例,实际上生成图片的提示词需要十分详尽,以确保生成的图片符合预期。而不是现在这样简单的一句话。

// 创建地图元素:边界墙
game.resources.generateImage("wall", "A top-down view of a stone wall, gray stones with moss, high detail");
game.resources.createElement("wall", {
    imageId: "wall",
    sizeX: 1,
    sizeY: 1,
    passable: false,
    onStep: null
    onInteract: null,
});

game.playStory({
    backgroundImageId: "grass", // 使用刚刚创建的草地图片作为背景
    initialState: {
        playerName: null,
        chestOpened: false,
        frameCount: 0,
        finished: false,
    },
    storyTitle: "初来乍到",
    storyDescription: "与陌生人交谈",
    isStoryFinished: (game, state) => {
        return state.playerName !== null && state.finished;
    },
    // 左上角是(0,0),因此玩家的初始位置是(1.5, 18.5)这样玩家会位于上面图中最左下角的格子的中央
    initialPlayerX: 1.5,
    initialPlayerY: 18.5,
    drawMap: (game, setElement) => {
        // 绘制墙
        for (let x = 0; x < 20; x++) {
            // 最上面和最下面的墙
            setElement(x, 0, game.resources.getElement("wall"));
            setElement(x, 19, game.resources.getElement("wall"));
            // 中间的隔断
            setElement(x, 9, game.resources.getElement("wall"));
            setElement(x, 10, game.resources.getElement("wall"));
        }
        for (let y = 0; y < 20; y++) {
            // 左右两侧的墙
            setElement(0, y, game.resources.getElement("wall"));
            setElement(19, y, game.resources.getElement("wall"));
        }
        // 绘制宝箱
        setElement(8, 4, {
            imageId: "chest",
            sizeX: 2,
            sizeY: 2,
            passable: true,
            onStep: null,
            onInteract: (game, state) => {
                if (state.chestOpened) {
                    game.env.getPlayer().showMessage("宝箱已经被打开了。");
                    return state;
                }
                state.chestOpened = true;
                game.env.getPlayer().showMessage("你打开了宝箱,获得了一个护身符!");
                game.env.giveItem({
                    name: "护身符",
                    imageId: "amulet",
                    effect: (game, breakItem, state) => {
                        const player = game.env.getPlayer();
                        // 每帧运行,增加3点最大生命值
                        player.setMaxHealth(player.getMaxHealth() + 3);
                        // 如果玩家死亡,则复活,然后销毁该护身符
                        if (player.getHealth() <= 0) {
                            player.setHealth(player.getMaxHealth());
                            game.env.getPlayer().showMessage("护身符复活了你!");
                            breakItem();
                        }
                        return state;
                    },
                    description: (state) => "一个神秘的护身符,可以增加3点最大生命值,并且在你死亡时复活你一次。",
                    tags: ["amulet", "revive"],
                    // 当装备其他护身符时不可再装备此护身符
                    equippable: (currentEquippedItems) => {
                        return !currentEquippedItems.some(item => item.tags.includes("amulet"));
                    },
                }, {});
                return state;
            },
        });
        // 绘制治疗区域
        setElement(8, 14, {
            imageId: "healing_spring",
            sizeX: 2,
            sizeY: 2,
            passable: true,
            onStep: (game, state) => {
                const player = game.env.getPlayer();
                // 每0.5秒治疗1点血量(注意游戏每秒20帧,因此每10帧治疗1点血量)
                state.frameCount++;
                state.frameCount %= 10;
                if (state.frameCount === 0) {
                    if (player.getHealth() < player.getMaxHealth()) {
                        player.setHealth(player.getHealth() + 1);
                    }
                }
                return state;
            },
            onInteract: null,
        });
        // 绘制传送门
        setElement(15, 14, {
            imageId: "portal",
            sizeX: 2,
            sizeY: 2,
            passable: true,
            onStep: (game, state) => {
                // 传送玩家到左上角(1.5, 1.5)
                game.env.movePlayer(1.5, 1.5)
                return state;
            },
            onInteract: null,
        });
    },
    putNPCs: (game, placeNPC) => {
        placeNPC(15.5, 4.5, {
            npc: {
                id: "friendly_npc",
                name: "友好的陌生人",
                imageId: "npc_image",
                personality: "这是一个xxxxxx世界,你是这个世界中的一个路人,你性格友好,乐于助人,喜欢和别人交朋友。", // 一般来说,你需要告知世界观等,以让NPC更好地融入世界,以及当玩家问及世界信息的时候NPC能给出合理,并且正确的回答。
            },
            messageToNPC: "你现在在树林里散步,遇到了玩家,你的任务是询问玩家的名字,在得到玩家的名字后,调用name工具,告诉我玩家的名字。然后询问玩家是否还有其他问题,当玩家确认没有问题后,调用finish工具,结束当前任务。",
            tools: [
                {
                    id: "name",
                    description: "询问玩家的名字后,调用这个工具,传入一个对象: { playerName: string }。",
                    onCall: (game, arg, storyState) => {
                        return {
                            msg: `玩家名字已经被记录为 ${arg.playerName}。`,
                            newState: { ...storyState, playerName: arg.playerName },
                        };
                    },
                },
                {
                    id: "finish",
                    description: "当玩家确认没有其他问题后,调用这个工具,结束当前任务。",
                    onCall: (game, arg, storyState) => {
                        if (!storyState.playerName) {
                            return {
                                msg: "你还没有得到玩家的名字,不能结束任务。请先询问玩家的名字。",
                                newState: storyState,
                            };
                        }
                        return {
                            msg: "任务完成",
                            newState: { ...storyState, finished: true },
                        };
                    },
                },
            ],
        });
    },
});

以上是一个完整的示例脚本,展示了如何使用js来向玩家呈现游戏。
在这个脚本中,一次性完成了从图片生成到最终呈现故事,但实际上,你可以拆分多次,而不必要一次性完成所有内容。拆分多次可以更好的控制,避免中途失败导致全都作废。
例如:
第一次:game.resources.generateImage(“grass”, “A top-down view of a grassy field, bright and vibrant colors, high detail”);
第二次:game.resources.createElement(“wall”, { imageId: “wall”, sizeX: 1, sizeY: 1, passable: false, onStep: null, onInteract: null, });

第三次:game.playStory({ … });
如果你需要获取一些信息,例如获取当前的npc列表,你可以使用console.log,若控制台有输出,则将返回给你
例如: console.log(game.resources.getNPCs());

另外,当调用game.playStory后,程序将进入游戏状态,直到目标完成后,才会将最终的state返回给你。因此在上面的示例中,你调用完game.playStory后,玩家将进入游戏,最终在任务完成后,程序会将最终的state返回给你。如:

{
    playerName: "XXXX",
    chestOpened: true,
    frameCount: 7,
    finished: true,
}

在剧情中,你可以多给出一些分支,例如让NPC询问玩家愿意做A还是B。或者直接弄两个NPC,让玩家选择和哪个NPC对话等。
之后将这些信息存储在state中,你再通过state来决定后续剧情的发展。

另外需要特别注意:

  1. 你编写的所有函数(包括effect,onStep,onInteract,onCall等)都不得使用任何外部的变量或函数,仅能使用传入的参数和内部变量,否则将会报错!这是为了保证脚本的安全性,防止脚本执行时访问外部环境。
    例如:
let x = 10;
setElement(..., {
    ...
    onStep: (game, state) => {
        x // 报错!!!
        return state;
    },
    ...
});
2. 所有的必须使用箭头函数(即 ()=>{} 这种)禁用function关键字定义的函数
3. 所有的state对象(包括story state,item state等)都必须可以被JSON.stringify序列化并通过JSON.parse恢复。否则会在需要存储state时出错。例如,state中不能包含函数,不能包含循环引用等。
4. 上述API可以做出很多效果,例如没隔固定时间给玩家回血,让NPC在听到玩家的特定回答后给玩家东西,解密获得答案告诉NPC触发后续剧情等。请善用这些API来丰富你的游戏内容。
 */
上面的接口中给的示例代码的效果(没有设计player的外观,暂时用了npc的,图片提示词写的非常简单,导致图片效果一般

当前的给AI的自由度还是不够高,像是战斗系统什么的,基本上AI只有权改个参数,并不能自定义战斗的操作什么的,距离我希望的完全AI还非常遥远。现在我这套设计基本是「可用的」,AI可以做出游戏 让ds设计剧情和编写脚本 。然而ai写出的剧情貌似还不太行。Ai生成的图片美术风格上也很难美观、统一

我目前在考虑拆分游戏脚本编写和剧情设计,变成一个agent设计剧情,再把剧情交由专门编写游戏脚本的agent,编写游戏脚本呈现给玩家。这样可以让负责剧情的AI不被干扰,能一定程度上提高一点剧情质量。

10 Likes

好家伙,你这是要创造元宇宙啊?无限剧情+全AI生成,这要成了游戏界得地震。不过AI生成的剧情深度和美术统一性确实是个大坑,期待你的阶段性成果!

1 Like

o 还有一个问题是天价花费,一张图片生成几毛钱,LLM的token的钱,基本上这个游戏跑起来就是在烧钱

4 Likes

所以这游戏不光烧显卡,还烧钱包啊?真正的“氪金”游戏了属于是。感觉可以加个“一键贷款”功能了

1 Like

image
更新系统看到这个…还差个antigravity就齐了

2 Likes

是的
抱歉有段时间没上论坛

1 Like

主要是感觉现在gpt回复没有思考过程,略难受

1 Like

现在问题是,我的gpt是白嫖的 poe.com 并非官方。他那个根本不提供response的接口。所以增加了response接口的支持现在基本上没有任何用,只能闲置

1 Like

现在gpt好像会输出思考啊?虽然我没用过官网gpt并不知道这是不是真正的思考过程:thinking:

1 Like

可以搞个codex上去

1 Like

为什么麦当劳和肯德基在早上都没有正餐。我非常需要在早上吃晚餐

3 Likes

试试汉堡王吗:thinking:比较接近正餐形式的早餐

1 Like