游戏套接字
该库已发布早期访问版,目前不稳定,因为它与其他解决方案并行开发。英语不是我的母语,所以没有注释。在这个阶段,该库适合那些想理解源代码并获得解决方案起点或帮助我的人:)
功能
- 一个库包含服务器端和客户端端。
- API通信库类似于
Socket.io,但不兼容该解决方案。 - 包含内置的二进制协议,因此您无需进行字节级别操作。
- 传输层使用
TCP。为了发送游戏消息,计划并行实现UDP。 - 它实现了多路复用等概念——通过单个通道与多个空间进行交互。
当前不计划支持WebSocket(但一切都可能随着社区的支持而改变)
示例
示例
用法
创建客户端
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);
}
}
}
此客户端连接到服务器上的主/命名空间,然后连接到/home命名空间。然后,它发送一个进入lobby房间的请求,之后它分派一个包含消息文本hello all的hello事件。
创建服务器
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]
客户端日志
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}
协议
协议是基于模式的。这种方法允许您节省传输的数据量,因为数据类型不随消息一起传输,并且数字的长度也不被序列化。
模式中使用的数据类型
| 类型 | Size | 范围 |
|---|---|---|
| 布尔值 | 1 位 | 真或假 |
| int8 | 1 字节 | 0 到 255 |
| int16 | 2 字节 | 0 到 65535 |
| int32 | 4 字节 | 0 到 4294967295 |
| string | 1 + 值 | 0 到 255 个字符 |
| bytes | 2 + 值 | 0 到 65535 字节 |
创建模式
import 'package:game_socket/protocol.dart';
typedef PS = PlayerStateSchema;
class PlayerStateSchema extends SimpleSchema {
@override
int get code => 10; // unique schema code 10..255
@override
int get version => 1; // version 0..255 to support game clients with different versions
// bool
static const int reserved = 0; // reserved
@override
int get boolCount => 1;
// int8
static const int speed = 0; // 0.000..1.000
static const int health = 1; // max(100)
@override
int get int8Count => 2;
// int16
static const int uid = 2; // max(65535)
static const int angle = 3; // radians
static const int score = 4; // max(65535)
@override
int get int16Count => 3;
// int32
static const int elapsedTime = 5; // time for internal synchronization
static const int x = 6; // x-coordinate
static const int y = 7; // y-coordinate
@override
int get int32Count => 3;
// strings
static const int name = 0; // player name
@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
}
}
读写消息时的数据类型
| 操作 | 模式类型 | Dart 类型 | 范围 |
|---|---|---|---|
| putBool | 布尔值 | 布尔值 | 真或假 |
| putInt | int8 | 整数 | -128 到 127 |
| putUInt | int8 | 整数 | 0 到 255 |
| putInt | int16 | 整数 | -32768 到 32767 |
| putUInt | int16 | 整数 | 0 到 65535 |
| putInt | int32 | 整数 | -2147483648 到 2147483647 |
| putUInt | int32 | 整数 | 0 到 4294967295 |
| putString | string | 字符串 | 0 到 255 个字符 |
| putSingle | ~int8~ | 双精度 | 0 到 1 步长 ~0.004 |
| putRadians | ~int16~ | 双精度 | 步长 ~0.0002 |
| putPayload | bytes | Uint8List | 0 到 65535 字节 |
计划
- 初始化以发送
UDP图。 - 自动连接和重新连接。
- 扩展与房间交互的可能性。
- 进行压力测试。
新手提示
- 如果您正在开发浏览器游戏,那么您需要一个
WebSocket解决方案。 - 在设计实时通信游戏时,
UDP是首选,因为TCP在数据包丢失时会导致延迟。