|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?注册
×
作者:微信文章
用微信小程序快速地验证了游戏的核心逻辑后,我就开始着手开发了。我要验证一种模式:一个人和AI合作开发一款游戏。首先是UI的设计。我的想法是,为了尽量的降低成本和提高效率,游戏尽量的少用,甚至不用动画。所以选择了“文字冒险游戏”这个类型。但UI是必须的。我打开豆包,输入了提示词:我要做个AI动态叙事类微信小游戏,游戏首页会展示故事的一个片段,以及三个选择,AI根据用户不同的选择进行故事的推进。用户可以在这个页面选择不同的故事进行游戏,也可以选择查看自己的游戏历史。请帮我设计这个页面,要求交互方便有新意,美观大方
豆包开始给的都是UI的文字描述,后来我提出了明确的绘制要求,它给出了UI图像。
经过反复的交互、修改,最终游戏的首页UI定稿如下:
然后就是“故事推进页”的UI。豆包开始给的也是文字描述:
我让豆包进一步描述:
最后考虑到这个游戏基本上是玩家和AI一起互动展开一个故事,仿佛看一本动态生成的书,所以最后定稿为一张书页的样式:
这里面有个技巧,有时候我们可能没有明确的方向,可以让AI发散性思维,也许会有意向不到的收获。
根据它给出的方案,我们通过交互,进行选择和完善。
先是文字方案,然后你可以提出绘制图像的要求(这也是我用豆包设计UI的原因,感觉有点像AGI了,可以在文字和图像之间随时转换)。
还可以让它提供代码,虽然这些代码你不一定用,但会给你很好的参考。
代码方面,我选择了DeepSeek,感觉这方面DS能力还是要强一点。我的策略是“由大到小,由粗到细”,从开放性的问题入手,逐渐细化,得到你想要的东西。首先我让DS帮我设计一个方案:我想做一款文字avg游戏,我是独立开发者,策划、美工、程序都是我一个人,我想利用ai的能力完成这款游戏,并上线运营,考虑到成本问题,我选择微信小游戏这个渠道。请帮我分析这类游戏的行情走向,设定一个选题,并指导我一步一步地完成这个游戏,我希望你的指导是细致可行的,最好是细化到每天的工作,我希望用1到2个月时间完成这个游戏的开发。请首先规划一个详细的方案提纲
然后让它帮我编排任务:我对cocoscreator比较熟悉,可以选择这个工具,我是一名游戏新手,我希望得到你的非常细致的指导,粒度最好到每一个任务,请帮我规划第一周的工作任务列表
我又让它帮我设计策划文案,当然游戏的名字我并没有要求它修改,这个最后自己决定就好,我怕这些细节的修改会打乱它的思路。好的,我们现在开始第一个工作。你设定的游戏《回声重构者》我很喜欢,请帮我完成这个游戏完整的策划文档,因为我没有游戏开发的经验,你还要指导我,如何根据这个文档进行开发,并组织美术和音乐等素材
然后让它设计程序架构。这里大家可能发现了提示词的一个特点:我扮演了一个初学者角色,因为我发现这样DS给出的解答会非常详细,常有意想不到的内容给你。你的执行文档非常好,很详细了,但是因为我是第一次开发游戏,有一些困惑,比如主线剧情大纲第一章,我要在程序中如何实现。文档好像没有具体说明,我做过传统管理软件的开发,一般都是有一个原型图给程序员参考开发,我知道用cocoscreator开发游戏,有个场景的概念,我是否要给第一章创建一个场景呢?请你给我具体的指导
而且还给出了详细的代码:
对于一些核心模块,可以单独处理。这个游戏的核心是玩家和AI的互动,为了更好的体验,就要流式输出,要用到websocket,开始的解决方案不好,不稳定,然后让DS进行了优化:我的cocoscreator3.8做的微信小游戏,后端是springboot做的服务接口,要用到websocket,但连接不稳定,频繁掉线,也没有处理好异常,经常断线后游戏画面就静止在哪里。请帮我设计一个稳定,异常处理完备的前后端代码
下面是后端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(); }}这只是一次很普通的尝试,有了一点小小的心得,这里分享出来,希望是抛砖引玉,只有更多的交流和碰撞,我们才能更快地进步。游戏现在已经上线,大家可以看看游戏的最终效果。
|
|