📜 快速开始
你可点击发行版本中的.bat文件来快速启动引擎,相关响应会在控制台中显示。
1.1 下载引擎
点击这里下载最新的您系统的引擎发行版
1.2 启动引擎
每个发行版本双击即可启动引擎。执行过程的日志和收发包都会记录在logs文件夹下。
1.3 脚本交互(python)
引擎监听8084端口,游戏端监听8199端口,引擎端和游戏端通过UDP数据包进行交互。
可以参照下面的python脚本👇来快速测试引擎的功能。
(由于UDP的包机制,我们通过一些trick来实现了大数据包的传输,具体的实现细节可以参考这里)
import socket
import json
import uuid
import time
import threading
"""
该脚本用于测试引擎的功能,包括:
1. 发送数据包到引擎8199端口(Game -> Engine)
2. 在8084监听回传给游戏的数据包(Game <- Engine)
"""
# 监听的地址和端口
UDP_IP = "::1"
UDP_PORT = 8084
# 发送数据的地址和端口
engine_url = "::1"
engine_port = 8199
# 创建socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
# 准备数据包
init_packet = {
"func": "init",
"scene_name": "雁栖村",
"language": "C",
"npc": []
}
wakeup_packet = {
"func": "wake_up",
"npc_name": "王大妈",
"scenario_name": "李大爷家",
"npc_state": {
"position": "李大爷家卧室",
"observation": {
"people": ["李大爷", "村长", "隐形李飞飞"],
"items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"],
"locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"]
},
"backpack": ["优质西瓜", "大砍刀", "黄金首饰"]
},
"time": "2021-01-01 12:00:00",
}
def send_data(data, max_packet_size=6000):
# UUID作为消息ID
msg_id = uuid.uuid4().hex
# 将json字符串转换为bytes
data = json.dumps(data).encode('utf-8')
# 计算数据包总数
packets = [data[i: i + max_packet_size] for i in range(0, len(data), max_packet_size)]
total_packets = len(packets)
for i, packet in enumerate(packets):
# 构造UDP数据包头部
header = f"{msg_id}@{i + 1}@{total_packets}".encode('utf-8')
# 发送UDP数据包
sock.sendto(header + b"@" + packet, (engine_url, engine_port))
print(f"Sent UDP packet: {header.decode('utf-8')}@{packet.decode('utf-8')}")
def listen():
print("Listening on [{}]:{}".format(UDP_IP, UDP_PORT))
while True:
data, addr = sock.recvfrom(4000)
# get json packet from udp
data = data.decode('utf-8')
json_str = data.split('@')[-1]
json_data = json.loads(json_str)
print("Received UDP packet from {}: {}".format(addr, json_data))
def send_packets():
while True:
send_data(init_packet)
send_data(wakeup_packet)
time.sleep(10)
# 分别启动监听和发送数据包的线程
threading.Thread(target=listen).start()
threading.Thread(target=send_packets).start()
📜 UDP数据包收发
Engine目前发送的响应UDP包是会动态扩增的,如果响应超过了最大包限制,那么Engine就会拆分发送。
收发包的统一结构为:
# {msg_id}@{i + 1}@{total_packets}@{json_data}
# 例(引擎响应包): 4bfe6122618b41fa85c8a8eb3ab37993@1@1@{"name": "action", "action": "chat", "object": "李大爷", "parameters": "李大爷,您知道匈房在哪里吗?", "npc_name": "王大姐"}
2.1 收发包(python)
import uuid, json, socket
def send_data(data, max_packet_size=6000):
engine_url = "::1"
engine_port = 8199
game_url = "::1"
game_port = 8084
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.bind((game_url, game_port))
# UUID作为消息ID
msg_id = uuid.uuid4().hex
# 将json字符串转换为bytes
data = json.dumps(data).encode('utf-8')
# 计算数据包总数
packets = [data[i: i + max_packet_size] for i in range(0, len(data), max_packet_size)]
total_packets = len(packets)
for i, packet in enumerate(packets):
# 构造UDP数据包头部
header = f"{msg_id}@{i + 1}@{total_packets}".encode('utf-8')
# 发送UDP数据包
sock.sendto(header + b"@" + packet, (engine_url, engine_port))
sock.close()
# 准备数据包
init_packet = {
"func": "init",
# 必填字段,代表在什么场景初始化
"scene_name": "雁栖村",
"language": "C",
# 下面是🉑️选
"npc": []
}
wakeup_packet = {
"func": "wake_up",
"npc_name": "王大妈",
"scenario_name": "李大爷家",
"npc_state": {
"position": "李大爷家卧室",
"observation": {
"people": ["李大爷", "村长", "隐形李飞飞"],
"items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"],
"locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"]
},
"backpack": ["优质西瓜", "大砍刀", "黄金首饰"]
},
"time": "2021-01-01 12:00:00", # 游戏世界的时间戳
}
# 发送数据包(发送后观察日志或终端)
send_data(init_packet)
send_data(wakeup_packet)
2.2 收发包(C#)
在Unity中也是需要对数据包进行处理才可以进行发送的,下面是一个简单的Unity脚本,可以参考一下。
// 发送UDP包的逻辑
private void SendData(object data)
{
string json = JsonUtility.ToJson(data); // 提取数据data中的字符串信息
json = $"@1@1@{json}"; // 左添加头信息
byte[] bytes = Encoding.UTF8.GetBytes(json); // 对字符串数据编码
this.sock.Send(bytes, bytes.Length, this.targetUrl, this.targetPort); // 通过socket向目标端口发送数据包
}
this.listenThread = new Thread(Listen);
this.listenThread.Start();
this.thread_stop = false;
//接收UDP包的逻辑(UDP包拼接为完整包)
public void Listen()
{
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.IPv6Loopback, this.listenPort);
this.sock.Client.Bind(localEndPoint);
string receivedData = ""; # 初始化receivedData用于整合接收到的字符串
while (!this.thread_stop) // 持续监听引擎端发送的数据包
{
byte[] data = this.sock.Receive(ref localEndPoint); // 接收到数据包
string packet = Encoding.UTF8.GetString(data); // 将接收到的数据包转化为字符串
string[] parts = packet.Split('@'); // 将字符串按照@字符进行分段并得到片段序列
string lastPart = parts[parts.Length - 1]; // 片段序列中的最后一段对应引擎端要传送的具体内容
receivedData+=lastPart; // 将内容拼接到ReceivedData后面
if (receivedData.EndsWith("}")) //多包机制下,此为收到最后一个包
{
//接下来对整合好的ReceivedData做下列分支判断和后处理
if (receivedData.Contains("\"inited")) // 如果有inited字段
{
num_initialized += 1;
UnityEngine.Debug.Log($"Successful initialization. {num_initialized}");
}else if (receivedData.Contains("\"conversation")) // 如果有conversation字段
{
ReceiveConvFormat json_data = JsonUtility.FromJson<ReceiveConvFormat>(receivedData);
UnityEngine.Debug.Log($"Get Conversation.{JsonUtility.ToJson(json_data)}");
receivedConvs.Add(json_data); flag_ConvReceived = true;
}else if (receivedData.Contains("\"action")) // 如果有action字段
{
FullActionFormat json_data = JsonUtility.FromJson<FullActionFormat>(receivedData);
UnityEngine.Debug.Log($"Get Action. {JsonUtility.ToJson(json_data)}");
receivedActions.Add(json_data); flag_ActReceived = true;
}
receivedData = ""; //收到最后一个包,重置收到的内容
}
}
}