Flutter游戏网络通信插件game_socket的使用

发布于 1周前 作者 itying888 来自 Flutter

Flutter游戏网络通信插件game_socket的使用

该库是在早期访问阶段发布的,并且尚未稳定。由于它与其他解决方案并行开发,因此在某些方面可能还不成熟。英语不是我的母语,所以没有注释。目前,该库适用于那些希望理解源代码并以此为起点来构建自己的解决方案或帮助我的人。

特性

  • 一个库同时包含服务器端和客户端部分。
  • API通信库类似于Socket.io,但不兼容此解决方案。
  • 包含内置二进制协议,因此无需在字节级别工作。
  • 传输层使用TCP。计划实现与UDP并行工作以发送游戏消息。
  • 实现了多路复用的概念 - 通过单个通道与多个空间进行交互。

当前不计划支持WebSocket(但社区的支持可能会改变这一点)。

示例

创建客户端

此客户端连接到服务器上的主要/命名空间,然后连接到/home命名空间。接着它发送请求进入lobby房间,之后它会分发一个包含消息文本hello allhello事件。

import 'package:game_socket/client.dart';

void main() {
  var client = GameClientExample();
  client.connect('localhost', 3103);
}

class GameClientExample extends GameSocketClient {
  GameClientExample() {
    on(Event.handshake, (packet) => _onHandshake(packet));
    on(Event.roomPacket, (packet) => _onRoomPacket(packet));
  }

  void _onHandshake(Packet packet) {
    if (packet.namespace == '/') {
      sendMessage(ConnectRequest('/home'));
    } else if (packet.namespace == '/home') {
      sendMessage(JoinRoomRequest('lobby', namespace: '/home'));
    }
  }

  void _onRoomPacket(RoomPacket packet) {
    var roomName = packet.roomName;
    if (packet.joinRoom && roomName == 'lobby') {
      var msg = RoomEvent(roomName!, namespace: '/home', event: 'hello', message: 'hello all');
      sendMessage(msg);
    }
  }
}

客户端日志

open InternetAddress('127.0.0.1', IPv4) ReadyState.open
handshake Packet{[0.0 /], bit:516, bool:16, int:[0, 0, 60, 0, 0, 0], string:{3: 15466abe2006464e99b6c8b36f7f4ed8}}
>> Message{[/home] boolMask:4, int:[0, 0, 0, 0, 0, 0], string:{} null}
handshake Packet{[0.0 /home], bit:516, bool:16, int:[0, 0, 60, 0, 0, 0], string:{3: 15466abe2006464e99b6c8b36f7f4ed8}}
>> Message{[/home] boolMask:16, int:[0, 0, 0], string:{0: lobby} null}
>> Message{[/home] boolMask:512, int:[0, 0, 0], string:{0: lobby, 5: hello, 1: hello all} null}

创建服务器

import 'package:game_socket/server.dart';

void main() {
  var service = SocketServiceExample();
  service.listen();
}

class SocketServiceExample {
  late GameSocketServer server;
  late Namespace home;

  SocketServiceExample() {
    server = GameSocketServer(options: ServerOptions.byDefault()..supportRawData = true);
    home = server.of('/home');
    home.on(ServerEvent.connect, (data) => _onHomeConnect(data));
    home.on('hello', (packet) => _onHomeData(packet));
    //
    server.on(ServerEvent.connection, (socket) {
      print('/: connection $socket');
      socket.on(ServerEvent.connect, (data) => _onConnect(data[0], data[1]));
      socket.on(Event.disconnecting, (data) => _onDisconnecting(data));
      socket.on(Event.disconnect, (data) => _onDisconnect(data[0], data[1]));
      socket.on(Event.error, (data) => _onError(data));
      socket.on(Event.data, (data) => _onData(data));
      socket.on(Event.close, (data) => {_onClose(data)});
    });
    server.on(ServerEvent.error, (data) => {print('/: eventError $data')});
    server.on(ServerEvent.close, (data) => {print('/: serverClose $data')});
    server.on(ServerEvent.raw, (data) => {print('/: raw $data')});
    server.on(ServerEvent.createRoom, (data) => {print('/: createRoom $data')});
    server.on(ServerEvent.joinRoom, (data) => {print('/: joinRoom $data')});
    server.on(ServerEvent.leaveRoom, (data) => {print('/: leaveRoom $data')});
    server.on(ServerEvent.deleteRoom, (data) => {print('/: deleteRoom $data')});
  }

  void listen() {
    server.listen();
  }

  void _onHomeConnect(dynamic data) {
    print('/home: connect $data');
  }

  void _onHomeData(dynamic data) {
    print('/home: $data');
    if (data is RoomPacket && data.roomName != null) {
      home.broadcast(data, rooms: {data.roomName!});
    }
  }

  void _onConnect(String namespace, String socketId) {
    print('/: connect $socketId');
  }

  void _onDisconnecting(dynamic data) {
    print('/: disconnecting $data');
  }

  void _onDisconnect(String namespace, String reason) {
    print('$namespace: disconnect reason:$reason');
  }

  void _onError(dynamic data) {
    print('/: error $data');
  }

  void _onData(dynamic data) {
    print('/: $data');
  }

  void _onClose(dynamic data) {
    print('/: close $data');
  }
}

服务器日志

listen null Options{ port:3103 raw:true closeOnError:false }
/: connection GameClient{ 15466abe2006464e99b6c8b36f7f4ed8 ReadyState.open [137545126]}
/: createRoom 15466abe2006464e99b6c8b36f7f4ed8
/: joinRoom [15466abe2006464e99b6c8b36f7f4ed8, 15466abe2006464e99b6c8b36f7f4ed8]
/home: connect [/home, 15466abe2006464e99b6c8b36f7f4ed8]

协议

该协议基于模式化。这种方法允许减少传输的数据量,因为消息中不传递数据类型,且数字长度不会被序列化。

数据类型

类型 大小 范围
bool 1 bit true 或 false
int8 1 byte 0 到 255
int16 2 bytes 0 到 65535
int32 4 bytes 0 到 4294967295
string 1 + 值 0 到 255 字符
bytes 2 + 值 0 到 65535 字节

操作时的消息数据类型

操作 模式类型 Dart 类型 范围
putBool bool bool true 或 false
putInt int8 int -128 到 127
putUInt int8 int 0 到 255
putInt int16 int -32768 到 32767
putUInt int16 int 0 到 65535
putInt int32 int -2147483648 到 2147483647
putUInt int32 int 0 到 4294967295
putString string String 0 到 255 字符
putSingle int8 double 0 到 1 步长 ~0.004
putRadians int16 double 步长 ~0.0002
putPayload bytes Uint8List 0 到 65535 字节

计划

  • 初始化发送 UDP 图表。
  • 自动连接和重新连接。
  • 扩展房间工作的可能性。
  • 进行压力测试。

初学者提示

  • 如果你正在开发浏览器游戏,那么你需要一个 WebSocket 解决方案。
  • 在设计实时通信的游戏时,应优先考虑 UDP,因为 TCP 会在数据包丢失时导致延迟。

创建架构

当你创建一个架构时,你要做两件事:取名为单元格数组的编号,并确定数组长度为五种架构数据类型之一。

import 'package:game_socket/protocol.dart';
typedef PS = PlayerStateSchema;
class PlayerStateSchema extends SimpleSchema {
  @override
  int get code => 10; // 独特的架构代码 10..255 
  @override
  int get version => 1; // 版本 0..255 以支持具有不同版本的游戏客户端

  // bool
  static const int reserved = 0; // 保留
  @override
  int get boolCount => 1;

  // int8
  static const int speed = 0; // 0.000..1.000
  static const int health = 1; // 最大值 (100)
  @override
  int get int8Count => 2;
  // int16
  static const int uid = 2; // 最大值 (65535)
  static const int angle = 3; // 弧度
  static const int score = 4; // 最大值 (65535)
  @override
  int get int16Count => 3;
  // int32
  static const int elapsedTime = 5; // 用于内部同步的时间
  static const int x = 6; // x坐标
  static const int y = 7; // y坐标
  @override
  int get int32Count => 3;

  // strings
  static const int name = 0; // 玩家名称
  @override
  int get stringsCount => 1;
}

消息

创建消息类

class PlayerStateMessage extends Message {
  PlayerStateMessage(Player player, {required double elapsedTime}) : super(PS()) {
    putUInt(PS.uid, player.uid);
    putInt(PS.x, (player.positionBody.x * 1000).toInt()); // ~ -2000000.0000..+2000000.0000
    putInt(PS.y, (player.positionBody.y * 1000).toInt());
    putInt(PS.score, player.score);
    putSingle(PS.speed, player.speed);
    putRadians(PS.angle, player.rotationBody);
    putUInt(PS.elapsedTime, (elapsedTime * 1000).toInt()); // double ms
  }
}

更多关于Flutter游戏网络通信插件game_socket的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter游戏网络通信插件game_socket的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter游戏项目中使用game_socket插件来实现网络通信的代码案例。game_socket插件允许你通过WebSocket进行低延迟、高性能的网络通信,非常适合实时游戏场景。

首先,你需要在你的pubspec.yaml文件中添加game_socket依赖:

dependencies:
  flutter:
    sdk: flutter
  game_socket: ^最新版本号  # 请替换为实际最新版本号

然后运行flutter pub get来安装依赖。

接下来是一个简单的示例,展示如何使用game_socket进行客户端和服务器之间的通信。

服务器端代码(Node.js示例)

假设你使用Node.js和ws库来创建一个简单的WebSocket服务器:

// 安装ws库: npm install ws
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('Client connected');

  ws.on('message', (message) => {
    console.log(`Received: ${message}`);
    // Echo the message back to the client
    ws.send(`Server response: ${message}`);
  });

  ws.on('close', () => {
    console.log('Client disconnected');
  });
});

console.log('WebSocket server is running on ws://localhost:8080');

Flutter客户端代码

在你的Flutter项目中,你可以按照以下方式使用game_socket插件:

import 'package:flutter/material.dart';
import 'package:game_socket/game_socket.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Game Socket Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: GameSocketDemo(),
    );
  }
}

class GameSocketDemo extends StatefulWidget {
  @override
  _GameSocketDemoState createState() => _GameSocketDemoState();
}

class _GameSocketDemoState extends State<GameSocketDemo> {
  GameSocket? _socket;
  TextEditingController _controller = TextEditingController();
  String _response = '';

  @override
  void initState() {
    super.initState();
    initSocket();
  }

  void initSocket() {
    _socket = GameSocket('ws://localhost:8080');

    _socket!.onOpen = () {
      print('WebSocket connection opened');
      // Optionally send an initial message
      _socket!.send('Hello Server!');
    };

    _socket!.onMessage = (String message) {
      setState(() {
        _response = message;
      });
      print('Received message: $message');
    };

    _socket!.onClose = () {
      print('WebSocket connection closed');
    };

    _socket!.onError = (error) {
      print('WebSocket error: $error');
    };
  }

  void _sendMessage() {
    if (_socket!.readyState == WebSocketReadyState.OPEN) {
      _socket!.send(_controller.text);
      _controller.clear();
    } else {
      print('WebSocket is not open');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Game Socket Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            TextField(
              controller: _controller,
              decoration: InputDecoration(labelText: 'Message'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _sendMessage,
              child: Text('Send Message'),
            ),
            SizedBox(height: 16),
            Text('Server Response: $_response'),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _socket?.close();
    _controller.dispose();
    super.dispose();
  }
}

说明

  1. 服务器端:使用Node.js和ws库创建一个简单的WebSocket服务器,监听8080端口。服务器会接收客户端的消息并回显。

  2. Flutter客户端

    • 使用GameSocket类创建一个WebSocket客户端,并连接到ws://localhost:8080
    • 设置onOpenonMessageonCloseonError回调来处理连接状态、接收消息、连接关闭和错误。
    • 提供一个简单的UI,让用户可以输入消息并发送到服务器,同时显示从服务器接收到的响应。

这个示例展示了基本的WebSocket通信流程,你可以根据需要在游戏中扩展这些功能,比如处理更复杂的消息格式、添加心跳检测、处理重连逻辑等。

回到顶部