Skip to content

游戏开发实战

本指南将带你深入了解如何使用 Tiaoom 构建完整的游戏应用。我们将涵盖从网络层适配到游戏逻辑实现的各个方面。

1. 服务端:实现 Socket 通信层

Tiaoom 不绑定具体的网络协议,你需要实现 Message 接口来适配你的网络层(如 WebSocket)。以下是一个使用 ws 库的完整示例:

typescript
import { WebSocketServer, WebSocket } from "ws";
import { EventEmitter } from "events";
import { IMessage, MessageTypes, IMessagePackage, MessageEvents, Player, Room } from "tiaoom";

export class SocketManager extends EventEmitter implements IMessage {
  sockets: Array<{ socket: WebSocket; player: Player }> = [];

  constructor(server: any) {
    super();
    const wsServer = new WebSocketServer({ server });
    
    wsServer.on("connection", (socket) => {
      this.emit("ready");
      
      socket.on("message", (data: any) => {
        try {
          const packet = JSON.parse(data);
          // 处理登录消息,绑定 socket 和 player
          if (packet.type == 'player.login') {
            this.sockets.push({ socket, player: packet.data });
          } else {
            // 为其他消息附加发送者信息
            const player = this.sockets.find(s => s.socket == socket)?.player;
            packet.sender = player;
          }
          
          // 触发 message 事件供 Tiaoom 处理
          this.emit("message", packet, (err, data) => {
            if (err) console.error(err); // 消息处理错误,会通过 global.error 事件通知到前端
            else socket.send(JSON.stringify({ type: packet.type, data }));
          });
        } catch (err) {
          this.emit("error", err as Error);
        }
      });

      socket.on("close", () => {
        // 处理断开连接逻辑
        const index = this.sockets.findIndex((s) => s.socket == socket);
        if (index > -1) {
          const { player } = this.sockets[index];
          this.sockets.splice(index, 1);
          // 如果玩家所有连接都断开,通知 Tiaoom
          if (!this.sockets.some(s => s.player.id === player.id)) {
            this.emit("message", { type: MessageTypes.PlayerLogout, data: player });
          }
        }
        this.emit("close");
      });
    });
  }

  // 实现 send 方法,将消息路由到正确的客户端
  // 消息类型有三种情况:
  // 1. 发送给特定玩家(player. 开头),message.sender 是 Player
  // 2. 发送给房间内所有玩家(room. 开头),message.sender 是 Room
  // 3. 广播给所有连接(其他情况)
  send(message: IMessagePackage) {
    const payload = JSON.stringify({ 
      type: message.type, 
      data: message.data, 
      sender: message.sender 
    });

    if (message.type.startsWith('player.') && message.sender) {
      // 发送给特定玩家
      const player = message.sender as Player;
      this.sockets.filter(s => s.player.id === player.id)
        .forEach(({ socket }) => socket.send(payload));
    } else if (message.type.startsWith('room.') && message.sender) {
      // 发送给房间内的所有玩家
      const room = message.sender as Room;
      room.players.forEach(p => {
        this.sockets.filter(s => s.player.id === p.id)
          .forEach(({ socket }) => socket.send(payload));
      });
    } else {
      // 广播消息
      this.sockets.forEach(({ socket }) => socket.send(payload));
    }
  }
  
  close() { 
    /* 关闭服务器逻辑 */
  }
}

2. 服务端:编写游戏逻辑

你可以通过监听房间事件来编写具体的游戏逻辑。

typescript
import { Room, RoomPlayer } from "tiaoom";

export default function setupGame(room: Room) {
  // 监听自定义游戏命令
  room.on('command', (cmd) => {
    if (cmd.type === 'talk') {
      // 处理玩家发言
      room.emit('message', `玩家 ${cmd.sender.name} 说: ${cmd.data}`);
    }
  });

  // 监听游戏开始事件
  room.on('start', () => {
    room.emit('message', '游戏开始!');
    startTurn(room.players[0]);
  });

  function startTurn(player: RoomPlayer) {
    // 发送命令给客户端,通知轮到某人发言
    room.emit('command', { type: 'turn', data: { playerId: player.id } });
    
    // 设置超时逻辑
    setTimeout(() => {
      room.emit('message', `玩家 ${player.name} 超时`);
      // 下一个玩家...
    }, 30000);
  }
}

3. 客户端:集成与扩展

在客户端,你需要继承 Tiaoom 类并实现连接逻辑。

typescript
import { Tiaoom, TiaoomEvents, MessageTypes } from 'tiaoom/client';
import ReconnectingWebSocket from 'reconnecting-websocket';

export class GameClient extends Tiaoom {
  private socket?: ReconnectingWebSocket;

  constructor(private url: string) {
    super();
  }

  connect() {
    this.socket = new ReconnectingWebSocket(this.url);
    
    this.socket.onopen = () => this.emit('sys.ready');
    
    this.socket.onmessage = ({ data }) => {
      const msg = JSON.parse(data);
      // 将 WebSocket 消息转发给 Tiaoom 事件系统
      this.emit(msg.type as keyof TiaoomEvents, msg.data, msg.sender);
    };
    
    this.socket.onclose = () => this.emit('sys.close');
  }

  send(msg: { type: MessageTypes; data?: any }) {
    this.socket?.send(JSON.stringify(msg));
  }

  // 扩展自定义方法
  say(content: string, roomId: string) {
    this.command(roomId, { type: 'talk', data: content });
  }
}

// 使用示例
const client = new GameClient('ws://localhost:3000');
client.connect();

client.on('sys.ready', () => {
  client.login({ id: '1', name: 'Player1' });
});

client.on('room.message', (msg) => {
  console.log('收到消息:', msg);
});

4. 其他接口示例

以下是一些常见的前端功能实现示例,包括房间列表、玩家列表和聊天功能。

获取房间列表

客户端可以通过监听 room.list 事件来获取实时更新的房间列表。

typescript
// 监听房间列表更新
client.on('room.list', (rooms) => {
  console.log('当前房间列表:', rooms);
  // 更新 UI
  updateRoomListUI(rooms);
});

// 主动请求房间列表(通常在连接建立后会自动推送,也可以手动触发)
// 值会在 `room.list` 事件中返回
client.send({ type: 'room.list' });

// 获取当前房间列表的快照
console.log(client.rooms);

获取在线玩家列表

类似地,可以通过监听 player.list 事件获取在线玩家列表。

typescript
// 监听玩家列表更新
client.on('player.list', (players) => {
  console.log('当前在线玩家:', players);
  // 更新 UI
  updatePlayerListUI(players);
});

// 主动请求玩家列表(通常在连接建立后会自动推送,也可以手动触发)
// 值会在 `player.list` 事件中返回
client.send({ type: 'player.list' });

// 获取当前在线玩家的快照
console.log(client.players);

实现聊天功能

聊天功能通常分为“大厅聊天”(全局)和“房间聊天”。

全局聊天

typescript
// 发送全局消息
function sendGlobalMessage(text) {
  // 发送一个类型为 'say' 的全局命令
  client.command({ type: 'say', data: text });
}

// 监听全局消息
client.on('global.command', (cmd) => {
  if (cmd.type === 'say') {
    console.log(`[全服] ${cmd.sender.name}: ${cmd.data}`);
    // 显示在聊天窗口
    appendChatMessage('全服', cmd.sender.name, cmd.data);
  }
});

房间聊天

typescript
// 发送房间消息
function sendRoomMessage(roomId, text) {
  // 发送一个类型为 'say' 的房间命令
  client.command(roomId, { type: 'say', data: text });
}

// 监听房间消息(需要在服务端处理转发,或者使用 room.message 事件)
// 如果服务端将 'say' 命令转换为 'message' 事件广播:
client.on('room.message', (msg) => {
   console.log(`[房间] ${msg.sender.name}: ${msg.data}`);
});

// 或者监听房间命令(如果服务端直接转发命令):
client.on('room.command', (cmd) => {
  if (cmd.type === 'say') {
    console.log(`[房间] ${cmd.sender.name}: ${cmd.data}`);
  }
});

房间管理

Tiaoom 处理了房间管理的繁重工作: