N 年没更新的我今天来更新了(什么?著名鸽子王更新文章了?)在鸽了 3 个月后我又回来了 🤔
在阅读这篇文章之前,如果你不知道什么是 WebSocket 的话,可以看看我的这篇文章:https://lo-li.cn/444 (演示已经挂了,看文章内容就好了)
众所周知我有一个 G27 方向盘(众人:谁知道啊),我之前还在 Bilibili 发视频演示过,因为没钱买外挂仪表,那玩意淘宝卖 900 多,于是我就打算自己造一个功能差不多的,同时又不需要多花一分钱的自制仪表。
这里我就发一个和本文相关的视频吧,配合视频阅读本文效果更佳:https://www.bilibili.com/video/av58089964/
首先我想到的是用真车仪表来改,不过马上这个想法就被 Pass 掉了,因为真车仪表太大,没地方放,我桌子不大;然后就是我家里也没有拆下来的汽车仪表,去外面收个二手的也要几十块钱吧,而我的目的是不想花钱又能搞到一个仪表盘。
然后我看到了窗台上放了半年多的华为畅想 8,马上就 jio 得有点搞头,然后就想到了用手机当仪表盘,大小刚好合适。那手机用什么做仪表应用呢?我不擅长 Android 开发,但是做个网页还是可以的,前端虽然水的一比但是能凑合。
OK,那既然是网页,用什么和游戏实时同步数据呢?Ajax?不,那玩意效率低的吓人,轮询起来服务器得炸,那还有什么技术呢?不用说,裆燃是 WebSocket 啦。说到 WebSocket,我就想到服务端,说到服务端,我就想到连接,说到连接我就想到万里传输数据,说到万里我就想到西天取经,说到西天取经我就想到西游记,今年下半年……开花……
不扯了,本次选用的技术是 PHP Swoole,Swoole 是一个优秀的多线程 PHP 扩展,使用 Swoole 可以快速开发多线程的 PHP 服务器应用,它集成了很多常见的服务端功能,例如 Http 服务器,WebSocket 服务器,因此只需要很少一部分代码就可以实现你要的功能。
首先用一张图来概括一下设计架构和思路:
因为 FiveM 实际上就是 GTA5 套了个 Chromium 浏览器,所以我们可以在客户端进行任何普通浏览器能进行的操作,这就包括了 WebSocket。
我们所需要的是一个中间服务器,FiveM 客户端会与中间服务器建立 WebSocket 连接,然后实时将车辆数据推送到中间服务器,然后手机打开网页,也连接到中间服务器,此时中间服务器就可以将 FiveM 客户端推送上来的车辆数据再转推给手机,手机接收到之后转换为仪表数据并显示。
前排温馨提示:不想看文章只想要资源的伸手党请直接跳到文章结尾
这里为了方便,我就用了一个速度计插件来进行修改,我们来看 FiveM 服务端代码(client.lua):
Citizen.CreateThread(function()
-- 创建死循环
while true do
-- 延迟 1 毫秒
Wait(1)
-- 取得玩家的实体
playerPed = GetPlayerPed(-1)
if playerPed then
-- 获得玩家驾驶的车辆实体
playerCar = GetVehiclePedIsIn(playerPed, false)
-- 判断玩家是否在车上,以及是否在驾驶位
if playerCar and GetPedInVehicleSeat(playerCar, -1) == playerPed then
-- 获得车辆发动机转速
carRPM = GetVehicleCurrentRpm(playerCar)
-- 获得车辆当前速度
carSpeed = GetEntitySpeed(playerCar)
-- 获得车辆当前的档位
carGear = GetVehicleCurrentGear(playerCar)
-- 获得车辆的转向灯状态
carIL = GetVehicleIndicatorLights(playerCar)
-- 获得车辆的手刹状态
carHandbrake = GetVehicleHandbrake(playerCar)
-- 获得车辆的大灯状态,三个变量对应的是(是否有车灯,是否开启车灯,是否开启远光灯)
carLS_r, carLS_o, carLS_h = GetVehicleLightsState(playerCar)
-- 将车辆速度进行计算,获得公里和英里速度
speedKmh = math.ceil(carSpeed * 3.6)
speedMph = math.ceil(carSpeed * 2.236936)
SendNUIMessage({
-- 向客户端的 Chromium 浏览器发送消息
showhud = true,
-- 这是一些插件自带的速度显示
unitLine1 = "KM/H",
speedLine1 = speedKmh,
unitLine2 = "MPH",
speedLine2 = speedMph,
-- 这一部分是要发送给客户端的信息
CurrentCarRPM = carRPM,
CurrentCarGear = carGear,
CurrentCarSpeed = carSpeed,
CurrentCarIL = carIL,
CurrentCarHandbrake = carHandbrake,
CurrentCarLS_r = carLS_r,
CurrentCarLS_o = carLS_o,
CurrentCarLS_h = carLS_h,
-- 玩家的 ID,后续辨识客户端使用
playerID = GetPlayerServerId(GetPlayerIndex())
})
else
-- 玩家不在载具内时,停止跟踪数据
SendNUIMessage({hidehud = true})
end
end
end
end)
接下来是 FiveM 客户端 JS 代码(main.js):
/* FiveMES configuration start */
var use_SSL = false;
var ws_host = "example.com";
var ws_port = 9230;
/* FiveMES configuration end */
/* Don't change anything following this line */
var s_playerID;
var s_rpm;
var s_speed;
var s_gear;
var s_IL;
var s_Handbrake;
var s_LS_r;
var s_LS_o;
var s_LS_h;
var inVehicle = false;
$(function() {
var ws;
var connected = false;
function websocket () {
pl = use_SSL ? "wss" : "ws";
ws = new WebSocket(`${pl}://${ws_host}:${ws_port}/`);
ws.onopen = function(event){
console.log('Connect successful');
connected = true;
};
ws.onmessage = function (event) {
var message = event.data;
}
ws.onclose = function(event) {
connected = false;
console.log("Connect close, status: " + this.readyState);
websocket();
};
ws.onerror = function(event) {
console.log("WebSocket error.");
};
}
function heartbeat() {
if(connected) {
var senddata = "{\"action\":\"heartbeat\",\"token\":\"" + $("#token").val() + "\"}";
ws.send(senddata);
return;
}
}
function setSpeed(id, speed, rpm, gear, IL, Handbrake, LS_r, LS_o, LS_h) {
if(connected) {
Handbrake = booltoString(Handbrake);
LS_r = booltoString(LS_r);
LS_o = booltoString(LS_o);
LS_h = booltoString(LS_h);
var senddata = `{"action":"setspeed","sid":"${id}","speed":"${speed}","rpm":"${rpm}","gear":"${gear}","IL":"${IL}","Handbrake":"${Handbrake}","LS_r":"${LS_r}","LS_o":"${LS_o}","LS_h":"${LS_h}"}`;
ws.send(senddata);
return;
}
}
function booltoString(str) {
return str ? "true" : "false";
}
websocket();
window.addEventListener("message", function(event) {
var item = event.data;
if (item.incar) {
inVehicle = true;
s_playerID = item.playerID;
s_rpm = item.CurrentCarRPM;
s_speed = item.CurrentCarSpeed;
s_gear = item.CurrentCarGear;
s_IL = item.CurrentCarIL;
s_Handbrake = item.CurrentCarHandbrake;
s_LS_r = item.CurrentCarLS_r;
s_LS_o = item.CurrentCarLS_o;
s_LS_h = item.CurrentCarLS_h;
} else if (item.outcar) {
inVehicle = false;
}
});
setInterval(function() {
if(inVehicle) {
setSpeed(s_playerID, s_speed, s_rpm, s_gear, s_IL, s_Handbrake, s_LS_r, s_LS_o, s_LS_h);
}
}, 50);
});
实际上你只需要修改 FiveMES configuration start
到 end 那一部分内容就行了,懒得注释了……大概的流程呢,就是客户端 Chromium 浏览器接收到 lua 脚本传来的 message 之后,通过 WebSocket 再发给中间服务器,中间服务器再转推给手机这个过程。
我们来看看中间服务器的代码(server.php):
< ?php
/**
*
* FiveMES by Akkariin
*
*/
if(PHP_SAPI !== 'cli') {
? >
<html lang="en">
<head>
<title>403 Forbidden</title>
<style type="text/css">
body {
background: #F1F1F1;
font-weight: 100 ! important;
padding: 32px;
}
h1 {
font-weight: 100 ! important;
}
logo {
font-size: 100px;
}
</style>
</head>
<body>
<logo>:(</logo>
<h1>403 Forbidden</h1>
<p><b>Error:</b> Plugin is disabled in php-fpm mode.</p>
<p><em>Powered by ZeroDream</em></p>
</body>
</html>
< ?php
exit;
}
// 储存服务器 ID => 连接 ID
$clientStorage = new Swoole\Table(2048);
$clientStorage->column('id', swoole_table::TYPE_INT, 4);
$clientStorage->column('client', swoole_table::TYPE_INT, 4);
$clientStorage->create();
// 储存连接 ID => 服务器 ID
$fidStorage = new Swoole\Table(2048);
$fidStorage->column('id', swoole_table::TYPE_INT, 4);
$fidStorage->column('client', swoole_table::TYPE_INT, 4);
$fidStorage->create();
$server = new swoole_websocket_server("0.0.0.0", 9230);
$server->clientStorage = $clientStorage;
$server->fidStorage = $fidStorage;
$server->on('open', function (swoole_websocket_server $server, $request) {
echo "Client {$request->fd} connected\n";
});
$server->on('message', function (swoole_websocket_server $server, $frame) {
$rawdata = $frame->data;
$data = json_decode($rawdata, true);
if($data) {
if(isset($data['action'])) {
switch($data['action']) {
case 'setclient':
if(isset($data['value']) && preg_match("/^[0-9]{1,5}$/", $data['value'])) {
if(!$server->clientStorage->get(Intval($data['value']))) {
$server->clientStorage->set(Intval($data['value']), Array('client' => $frame->fd));
$server->fidStorage->set($frame->fd, Array('client' => Intval($data['value'])));
echo "Client {$frame->fd} Connect to game ID {$data['value']}\n";
}
}
break;
case 'setspeed':
if(isset($data['sid']) && preg_match("/^[0-9]{1,5}$/", $data['sid'])) {
$fd = $server->clientStorage->get(Intval($data['sid']), 'client');
if($fd) {
if(isset($data['speed']) && isset($data['gear']) && isset($data['rpm'])) {
$rs = $server->push($fd, json_encode(Array(
'action' => 'update',
'speed' => Floatval($data['speed']),
'gear' => Intval($data['gear']),
'rpm' => Floatval($data['rpm']),
'IL' => Intval($data['IL']),
'Handbrake' => $data['Handbrake'],
'LS_r' => $data['LS_r'],
'LS_o' => $data['LS_o'],
'LS_h' => $data['LS_h']
)));
if(!$rs) {
$fid = $server->fidStorage->get($fd, 'client');
$server->fidStorage->del($fd);
$server->clientStorage->det($fid);
}
}
}
}
break;
default:
// Undefined action
}
}
}
});
$server->on('close', function (swoole_websocket_server $server, $fd) {
echo "Client {$fd} offline\n";
$fid = $server->fidStorage->get($fd, 'client');
if($fid) {
$server->fidStorage->del($fd);
$server->clientStorage->del($fid);
}
});
$server->start();
function emptyStr($str) {
if(empty($str) || $str == null) {
return "";
} else {
return $str;
}
}
function getBoolean($data) {
return $data == "true";
}
这里呢,我创建了两个 Table,用于储存用户信息,其中 clientStorage
表负责储存 服务器 ID => 浏览器标识
,而 fidStorage
表则负责储存 浏览器标识 => 服务器 ID
,你可能会疑问为什么要两个表,我们接着往下看看。
首先,浏览器打开后会要求你输入游戏中的 ID(这里就叫“服务器 ID”),输入之后,浏览器会与中间服务器建立 WebSocket 连接,并发送一个 setClient
的消息,参数就是你的服务器 ID,这就大概是告诉中间服务器:我是浏览器 XXX,我现在要查看服务器里 ID 为 YYY 的玩家的车辆数据。
中间服务器接收到了以后,就把 YYY 作为 Key 值,Value 为你的浏览器标识,储存到 clientStorage 表里,然后把你的浏览器标识作为 Key,Value 为你的服务器 ID,储存到 fidStorage 表里。
接下来,你打开游戏,进入服务器,此时客户端就会与服务器建立一个 WebSocket 连接,然后以每 50 毫秒一次的频率将你的车辆数据发送到中间服务器(setSpeed),并且附带你游戏中的 ID,中间服务器接收到了以后,根据这个传过来的 ID 在 clientStorage 里搜寻对应的浏览器标识,然后再给对应的浏览器传输你的车辆数据。
浏览器在接收到你的车辆数据之后,绘制转速表的指针,就可以实时显示了。
最后,话不多说,放上 Github 地址:https://github.com/kasuganosoras/FiveMES
希望大家玩的愉快,喜欢的话记得给我点个小星星~
大佬,Sakura Frp这几天为啥经常出问题啊…
维护啊
化腐朽为神奇啊!我觉得博主真的太牛了,想到牛,我就想起了牛魔王,想到了牛魔王,我就想到了铁扇公主,想到了铁扇公主,我就想到了红孩儿,也不知道这孩子现在过得怎么样。为了让孩子有个好前程,我让他认牛做父啊!
这么巨吗?博主多大啊?