找回密码
 注册

Sign in with Twitter

It's what's happening?

微信登录

微信扫一扫,快速登录

查看: 290|回复: 0

AI动态叙事游戏的最终实现

[复制链接]
发表于 2026-1-28 20:12 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册 微信登录

×
作者:微信文章
用微信小程序快速地验证了游戏的核心逻辑后,我就开始着手开发了。我要验证一种模式:一个人和AI合作开发一款游戏。首先是UI的设计。我的想法是,为了尽量的降低成本和提高效率,游戏尽量的少用,甚至不用动画。所以选择了“文字冒险游戏”这个类型。但UI是必须的。我打开豆包,输入了提示词:我要做个AI动态叙事类微信小游戏,游戏首页会展示故事的一个片段,以及三个选择,AI根据用户不同的选择进行故事的推进。用户可以在这个页面选择不同的故事进行游戏,也可以选择查看自己的游戏历史。请帮我设计这个页面,要求交互方便有新意,美观大方
w1.jpg
豆包开始给的都是UI的文字描述,后来我提出了明确的绘制要求,它给出了UI图像。
w2.jpg
经过反复的交互、修改,最终游戏的首页UI定稿如下:
w3.jpg
然后就是“故事推进页”的UI。豆包开始给的也是文字描述:
w4.jpg

我让豆包进一步描述:
w5.jpg
最后考虑到这个游戏基本上是玩家和AI一起互动展开一个故事,仿佛看一本动态生成的书,所以最后定稿为一张书页的样式:
w6.jpg
这里面有个技巧,有时候我们可能没有明确的方向,可以让AI发散性思维,也许会有意向不到的收获。
w7.jpg
根据它给出的方案,我们通过交互,进行选择和完善。
w8.jpg
先是文字方案,然后你可以提出绘制图像的要求(这也是我用豆包设计UI的原因,感觉有点像AGI了,可以在文字和图像之间随时转换)。
w9.jpg
还可以让它提供代码,虽然这些代码你不一定用,但会给你很好的参考。
w10.jpg
代码方面,我选择了DeepSeek,感觉这方面DS能力还是要强一点。我的策略是“由大到小,由粗到细”,从开放性的问题入手,逐渐细化,得到你想要的东西。首先我让DS帮我设计一个方案:我想做一款文字avg游戏,我是独立开发者,策划、美工、程序都是我一个人,我想利用ai的能力完成这款游戏,并上线运营,考虑到成本问题,我选择微信小游戏这个渠道。请帮我分析这类游戏的行情走向,设定一个选题,并指导我一步一步地完成这个游戏,我希望你的指导是细致可行的,最好是细化到每天的工作,我希望用1到2个月时间完成这个游戏的开发。请首先规划一个详细的方案提纲
w11.jpg
然后让它帮我编排任务:我对cocoscreator比较熟悉,可以选择这个工具,我是一名游戏新手,我希望得到你的非常细致的指导,粒度最好到每一个任务,请帮我规划第一周的工作任务列表
w12.jpg
我又让它帮我设计策划文案,当然游戏的名字我并没有要求它修改,这个最后自己决定就好,我怕这些细节的修改会打乱它的思路。好的,我们现在开始第一个工作。你设定的游戏《回声重构者》我很喜欢,请帮我完成这个游戏完整的策划文档,因为我没有游戏开发的经验,你还要指导我,如何根据这个文档进行开发,并组织美术和音乐等素材
w13.jpg
然后让它设计程序架构。这里大家可能发现了提示词的一个特点:我扮演了一个初学者角色,因为我发现这样DS给出的解答会非常详细,常有意想不到的内容给你。你的执行文档非常好,很详细了,但是因为我是第一次开发游戏,有一些困惑,比如主线剧情大纲第一章,我要在程序中如何实现。文档好像没有具体说明,我做过传统管理软件的开发,一般都是有一个原型图给程序员参考开发,我知道用cocoscreator开发游戏,有个场景的概念,我是否要给第一章创建一个场景呢?请你给我具体的指导
w14.jpg
而且还给出了详细的代码:
w15.jpg
对于一些核心模块,可以单独处理。这个游戏的核心是玩家和AI的互动,为了更好的体验,就要流式输出,要用到websocket,开始的解决方案不好,不稳定,然后让DS进行了优化:我的cocoscreator3.8做的微信小游戏,后端是springboot做的服务接口,要用到websocket,但连接不稳定,频繁掉线,也没有处理好异常,经常断线后游戏画面就静止在哪里。请帮我设计一个稳定,异常处理完备的前后端代码
w16.jpg
下面是后端Java的代码:@Slf4j@Componentpublic class GameWebSocketHandler extends TextWebSocketHandler {
    private final String apiKey = "你的AI API的Key";
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    // 心跳间隔(秒)    private static final long HEARTBEAT_INTERVAL = 30;
    private final IStoryInfoService storyInfoService;
    // 构造器注入    @Autowired    public GameWebSocketHandler(IStoryInfoService storyInfoService) {        this.storyInfoService = storyInfoService;    }
    @Override    public void afterConnectionEstablished(WebSocketSession session) throws Exception {        String sessionId = session.getId();        sessions.put(sessionId, session);        log.info("WebSocket连接建立, sessionId: {}, 当前连接数: {}", sessionId, sessions.size());
        // 发送连接成功消息        sendMessage(session, createMessage("CONNECT_SUCCESS", "连接成功"));
        // 启动心跳检测        startHeartbeat(session);    }
    @Override    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {        try {            String payload = message.getPayload();            log.debug("收到消息: {}", payload);
            WSPrams params = JSON.parseObject(payload,WSPrams.class);
            String type = params.getType();
            switch (type) {                case "HEARTBEAT":                    // 心跳回应                    handleHeartbeat(session);                    break;                case "GAME_DATA":                    // 处理游戏数据                    handleGameData(session, params.getData());                    break;                default:                    log.warn("未知消息类型: {}", type);            }        } catch (Exception e) {            log.error("AI消息处理异常", e);            sendMessage(session, createMessage("ERROR", "消息处理失败"));        }    }
    @Override    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {        log.error("WebSocket传输错误, sessionId: {}", session.getId(), exception);    }
    @Override    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {        String sessionId = session.getId();        sessions.remove(sessionId);        log.info("WebSocket连接关闭, sessionId: {}, 状态: {}, 当前连接数: {}",                sessionId, closeStatus, sessions.size());    }
    private void startHeartbeat(WebSocketSession session) {        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();        scheduler.scheduleAtFixedRate(() -> {            try {                if (session.isOpen()) {                    StoryResponse heartbeat = createMessage("HEARTBEAT_RESPONSE", "ping");                    sendMessage(session, heartbeat);                } else {                    scheduler.shutdown();                }            } catch (Exception e) {                log.error("发送心跳失败", e);                scheduler.shutdown();            }        }, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);    }
    private void handleHeartbeat(WebSocketSession session) {        StoryResponse response = createMessage("HEARTBEAT_RESPONSE", "pong");        sendMessage(session, response);    }
    /**     * 处理游戏业务逻辑     * @param session     * @param data     */    private void handleGameData(WebSocketSession session, Object data) throws Exception {
        // 1. 解析参数        JSONObject jsonData = (JSONObject) data;        StoryPrams params = JSON.parseObject(jsonData.toJSONString(), StoryPrams.class);
        // 2. 更新用户历史(不阻塞主流程)        this.storyInfoService.updateStoryHistoryDetail(params.getStoryInfoId(),params.getUserId(),params.getPlayerAction());
        // 3. 获取故事信息        StoryInfo storyInfo = this.storyInfoService.getAIStoryInfo(params.getStoryInfoId(),params.getUserId());
        // 4. 构建AI请求消息        List<Message> messages = buildMessages(storyInfo);
        // 5. 异步调用AI服务并流式返回        streamAIResponse(session, params, messages);
    }
    /**     * 4. 构建AI请求消息     */    private List<Message> buildMessages(StoryInfo storyInfo) {        List<Message> messages = new ArrayList<>();
        // 系统提示        messages.add(Message.builder()                .role(Role.SYSTEM.getValue())                .content(storyInfo.getSysPrompt())                .build());
        // 历史对话        List<StoryHistoryDetail> details = Optional.ofNullable(storyInfo.getHistory())                .map(StoryHistory::getDetails)                .orElse(Collections.emptyList());
        for (StoryHistoryDetail item : details) {            // 助理回复            if (StringUtils.isNotBlank(item.getContent())) {                messages.add(Message.builder()                        .role(Role.ASSISTANT.getValue())                        .content(item.getContent())                        .build());            }
            // 用户选择            if (StringUtils.isNotBlank(item.getOptionChoose())) {                messages.add(Message.builder()                        .role(Role.USER.getValue())                        .content(item.getOptionChoose())                        .build());            }        }
        // 记录请求日志        logAIMessageRequest(messages);
        return messages;    }
    /**     * 5. 流式调用AI服务     */    private void streamAIResponse(WebSocketSession session, StoryPrams params, List<Message> messages) {
        // 构建AI参数        GenerationParam param = buildGenerationParam(messages);
        // 异步调用AI服务        CompletableFuture<Void> aiFuture = CompletableFuture.runAsync(() -> {
            StringBuilder contentStr = new StringBuilder();            StringBuilder optionStr = new StringBuilder();            AtomicBoolean stopSend = new AtomicBoolean(false);
            try {                Generation gen = new Generation();                Disposable disposable = gen.streamCall(param)                        .timeout(30,TimeUnit.SECONDS)                        .doOnError(throwable -> {                            log.error("AI流式调用错误", throwable);                            sendMessage(session, createMessage("ERROR", "消息处理失败"));                        })                        .doOnTerminate(()->{                            log.debug("AI流式调用终止,sessionId: {}", session.getId());                        })                        .subscribe(                            // 处理每个chunk                            msg -> processAIChunk(msg, session,contentStr,optionStr,stopSend),                            // 错误处理                            error -> {                                cancelAIStream(session);                            },                            // 完成处理                            () -> handleAIStreamComplete(session, params, contentStr,optionStr)                        );
                // 保存disposable以便取消                session.getAttributes().put("ai_stream_disposable", disposable);                // 用于判断处理AI消息超时                session.getAttributes().put("ai_stream_process", true);
            } catch (Exception e) {                log.error("AI消息处理失败", e);                sendMessage(session, createMessage("ERROR", "消息处理失败"));            }        }, aiExecutor());

        // 设置超时处理        handleAITimeout(session, aiFuture);    }
    /**     * 处理超时     */    private void handleAITimeout(WebSocketSession session, CompletableFuture<Void> future) {
        // 首先取消之前的定时器        Object timeoutFutureObj = session.getAttributes().get("ai_stream_timeoutFuture");        Object schedulerObj = session.getAttributes().get("ai_stream_scheduler");        if(timeoutFutureObj!=null && schedulerObj!=null) {            ((ScheduledFuture) timeoutFutureObj).cancel(true);            ((ScheduledExecutorService) schedulerObj).shutdown();        }
        String sessionId = session.getId();
        // 设置超时时间(可配置)        long timeout = 30;
        // 创建超时调度任务        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();        ScheduledFuture<?> timeoutFuture = scheduler.schedule(() -> {            // 处理AI消息超时            Object processObj = session.getAttributes().get("ai_stream_process");            if(processObj != null){                log.error("AI消息异步任务超时,sessionId: {}", sessionId);
                // 取消原始任务                future.cancel(true);
                // 发送超时错误给客户端                sendMessage(session, createMessage("ERROR", "消息处理失败"));
                // 清理资源                cancelAIStream(session);            }        }, timeout, TimeUnit.SECONDS);
        // 保存scheduler以便取消        session.getAttributes().put("ai_stream_scheduler", scheduler);        session.getAttributes().put("ai_stream_timeoutFuture", timeoutFuture);
    }
    /**     * 清理资源     */    private void cancelAIStream(WebSocketSession session) {        String sessionId = session.getId();
        try {            // 1. 取消正在进行的AI流            Object disposableObj = session.getAttributes().get("ai_stream_disposable");            if (disposableObj instanceof Disposable) {                ((Disposable) disposableObj).dispose();                log.debug("已取消AI流,sessionId: {}", sessionId);            }
            // 2. 清理资源            session.getAttributes().remove("ai_stream_disposable");            session.getAttributes().remove("ai_stream_process");
        } catch (Exception e) {            log.error("处理会话超时失败", e);        }    }
    /**     * 处理AI流式返回的每个chunk     */    private void processAIChunk(GenerationResult msg, WebSocketSession session,StringBuilder contentStr,StringBuilder optionStr,AtomicBoolean stopSend) {        String content = msg.getOutput().getChoices().get(0).getMessage().getContent();
        log.debug("AI返回内容:{}",content);
        if(StrUtil.isBlank(content)) return;
        if(content.contains("{") && !stopSend.get()){            int index = content.indexOf("{");            if(index != -1){                String subContent = content.substring(0,index);                contentStr.append(subContent);                this.sendMessage(session,createMessage("GAME_UPDATE_CONTENT",subContent));                content = content.substring(index);            }            stopSend.set(true);        }
        if(stopSend.get()){            optionStr.append(content);        }        else {            contentStr.append(content);            this.sendMessage(session,createMessage("GAME_UPDATE_CONTENT",content));        }    }
    /**     * 处理AI流完成     */    private void handleAIStreamComplete(WebSocketSession session, StoryPrams params,StringBuilder contentStr,StringBuilder optionStr) {        String option = optionStr.toString();        String content = contentStr.toString();
        log.info("option:{}",option);
        // 保存结果        this.storyInfoService.addStoryHistoryDetail(params.getStoryInfoId(),params.getUserId(),content,option);
        // 发送选项给客户端        this.sendMessage(session,createMessage("GAME_UPDATE_OPTION",option));
        // 消息处理完毕        session.getAttributes().remove("ai_stream_process");    }
    /**     * 日志方法     */    private void logAIMessageRequest(List<Message> messages) {        log.debug("==== AI请求消息 ====");        messages.forEach(msg ->                log.debug("角色: {}, 内容: {}", msg.getRole(), msg.getContent())        );        log.debug("==== 结束 ====");    }

    public void sendMessage(WebSocketSession session, StoryResponse res) {        try {            if (session != null && session.isOpen()) {                synchronized (session) {                    session.sendMessage(new TextMessage(JSON.toJSONString(res)));                }            }        } catch (Exception e) {            log.error("发送消息失败", e);        }    }

    /**     * 广播消息给所有客户端     * @param message     */    public void broadcastMessage(StoryResponse message) {        sessions.values().forEach(session -> sendMessage(session, message));    }
    private StoryResponse createMessage(String type, String data) {        StoryResponse message = new StoryResponse();        message.setType(type);        message.setData(data);        message.setTimestamp(System.currentTimeMillis());        return message;    }
    private GenerationParam buildGenerationParam(List<Message> messages) {        return GenerationParam.builder()                .apiKey(apiKey)                .model("deepseek-v3.2-exp")                .enableThinking(false)                .incrementalOutput(true)                .resultFormat("message")                .messages(messages)                .build();    }

    @Bean("aiExecutor")    public Executor aiExecutor() {        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();        executor.setCorePoolSize(10);        executor.setMaxPoolSize(50);        executor.setQueueCapacity(200);        executor.setThreadNamePrefix("ai-");        executor.initialize();        return executor;    }
}前端代码:
export class WebSocketManager {    private ws: WebSocket | null = null;    private reconnectAttempts = 0;    private maxReconnectAttempts = 5;    private reconnectInterval = 3000;    private heartbeatInterval: number | null = null;    private isConnected = false;    private url: string = '';
    // 事件监听器    private eventListeners: Map<string, Function[]> = new Map();
    // 连接状态    public static readonly CONNECTING = 'connecting';    public static readonly CONNECTED = 'connected';    public static readonly DISCONNECTED = 'disconnected';    public static readonly ERROR = 'error';
    constructor() {        this.setupEventListeners();    }
    /**     * 连接WebSocket     */    public connect(url: string): void {        if (this.isConnected) {            console.warn('WebSocket已经连接');            return;        }
        this.url = url;        this.reconnectAttempts = 0;        this.doConnect();    }
    private doConnect(): void {        try {            this.emit('statusChange', WebSocketManager.CONNECTING);            console.log(`正在连接WebSocket: ${this.url}`);
            this.ws = new WebSocket(this.url);            this.setupWebSocketEvents();        } catch (error) {            console.error('创建WebSocket连接失败:', error);            this.handleError(error);        }    }
    private setupWebSocketEvents(): void {        if (!this.ws) return;
        this.ws.onopen = (event) => {            console.log('WebSocket连接成功');            this.isConnected = true;            this.reconnectAttempts = 0;            this.emit('statusChange', WebSocketManager.CONNECTED);            this.emit('connected', event);            this.startHeartbeat();        };
        this.ws.onmessage = (event) => {            try {                const message = JSON.parse(event.data);                this.handleMessage(message);            } catch (error) {                console.error('解析消息失败:', error, event.data);            }        };
        this.ws.onclose = (event) => {            console.log(`WebSocket连接关闭: ${event.code} - ${event.reason}`);            this.isConnected = false;            this.emit('statusChange', WebSocketManager.DISCONNECTED);            this.emit('disconnected', event);            this.stopHeartbeat();            this.handleReconnect();        };
        this.ws.onerror = (event) => {            console.error('WebSocket错误:', event);            this.isConnected = false;            this.emit('statusChange', WebSocketManager.ERROR);            this.emit('error', event);            this.handleError(event);        };    }
    /**     * 处理接收到的消息     */    private handleMessage(message: any): void {        const { type, data, timestamp } = message;
        switch (type) {            case 'HEARTBEAT_RESPONSE':                // 心跳回应,更新最后活跃时间                this.emit('heartbeat', data);                break;
            case 'GAME_UPDATE_CONTENT':                // 游戏数据更新                this.emit('gameUpdateContent', data);                break;
            case 'GAME_UPDATE_OPTION':                // 游戏数据更新                this.emit('gameUpdateOption', data);                break;
            case 'ERROR':                console.error('服务器返回错误:', data);                this.emit('serverError', data);                break;
            case 'CONNECT_SUCCESS':                console.log('服务器连接确认:', data);                this.emit('connectSuccess', data);                break;
            default:                console.warn('未知消息类型:', type);                this.emit('unknownMessage', message);        }
        // 触发通用消息事件        this.emit('message', message);    }
    /**     * 发送消息     */    public send(type: string, data: any): boolean {        if (!this.isConnected || !this.ws) {            console.error('WebSocket未连接,无法发送消息');            return false;        }
        try {            const message = {                type,                data,                timestamp: Date.now()            };
            this.ws.send(JSON.stringify(message));            return true;        } catch (error) {            console.error('发送消息失败:', error);            this.handleError(error);            return false;        }    }
    /**     * 开始心跳检测     */    private startHeartbeat(): void {        this.stopHeartbeat();
        // 每30秒发送一次心跳        this.heartbeatInterval = setInterval(() => {            if (this.isConnected) {                this.send('HEARTBEAT', 'ping');            }        }, 30000) as unknown as number;    }
    /**     * 停止心跳检测     */    private stopHeartbeat(): void {        if (this.heartbeatInterval) {            clearInterval(this.heartbeatInterval);            this.heartbeatInterval = null;        }    }
    /**     * 处理重连逻辑     */    private handleReconnect(): void {        if (this.reconnectAttempts >= this.maxReconnectAttempts) {            console.error(`已达到最大重连次数(${this.maxReconnectAttempts}),停止重连`);            this.emit('maxReconnectAttempts');            return;        }
        this.reconnectAttempts++;        const delay = this.reconnectInterval * this.reconnectAttempts;
        console.log(`${delay}ms后尝试第${this.reconnectAttempts}次重连...`);
        setTimeout(() => {            if (!this.isConnected) {                this.doConnect();            }        }, delay);    }
    /**     * 处理错误     */    private handleError(error: any): void {        this.emit('error', error);
        // 可以根据错误类型进行不同的处理        if (error instanceof Error) {            console.error('WebSocket错误详情:', error.message, error.stack);        }    }
    /**     * 关闭连接     */    public disconnect(): void {        this.stopHeartbeat();        this.isConnected = false;
        if (this.ws) {            this.ws.close(1000, '正常关闭');            this.ws = null;        }    }
    /**     * 事件监听相关方法     */    public on(event: string, callback: Function): void {        if (!this.eventListeners.has(event)) {            this.eventListeners.set(event, []);        }        this.eventListeners.get(event)!.push(callback);    }
    public off(event: string, callback: Function): void {        const listeners = this.eventListeners.get(event);        if (listeners) {            const index = listeners.indexOf(callback);            if (index > -1) {                listeners.splice(index, 1);            }        }    }
    private emit(event: string, data?: any): void {        const listeners = this.eventListeners.get(event);        if (listeners) {            listeners.forEach(callback => {                try {                    callback(data);                } catch (error) {                    console.error(`事件处理错误 ${event}:`, error);                }            });        }    }
    private setupEventListeners(): void {        // 监听页面可见性变化        document.addEventListener('visibilitychange', () => {            if (document.visibilityState === 'visible') {                // 页面重新可见时检查连接                if (!this.isConnected) {                    this.handleReconnect();                }            }        });
        // 监听网络状态变化        window.addEventListener('online', () => {            console.log('网络连接恢复,尝试重连WebSocket');            if (!this.isConnected) {                this.handleReconnect();            }        });
        window.addEventListener('offline', () => {            console.log('网络连接断开');            this.emit('networkOffline');        });    }
    /**     * 获取连接状态     */    public getConnectionStatus(): string {        if (this.isConnected) return WebSocketManager.CONNECTED;        return WebSocketManager.DISCONNECTED;    }
    /**     * 销毁实例     */    public destroy(): void {        this.disconnect();        this.eventListeners.clear();    }}这只是一次很普通的尝试,有了一点小小的心得,这里分享出来,希望是抛砖引玉,只有更多的交流和碰撞,我们才能更快地进步。游戏现在已经上线,大家可以看看游戏的最终效果。
w17.jpg
Die von den Nutzern eingestellten Information und Meinungen sind nicht eigene Informationen und Meinungen der DOLC GmbH.
您需要登录后才可以回帖 登录 | 注册 微信登录

本版积分规则

Archiver|手机版|AGB|Impressum|Datenschutzerklärung|萍聚社区-德国热线-德国实用信息网

GMT+1, 2026-2-23 06:56 , Processed in 0.119833 second(s), 31 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表