需求:
一個(gè)手機(jī)打飛機(jī)游戲,一個(gè)房間2個(gè)飛機(jī),然后各自控制飛機(jī)位置,子彈自動(dòng)發(fā)射,看誰先掛;
本來是下載了win版聊天室框架GatewayWorker來搞,正好都合適 ,有房間,有各種存儲,搞起來很順;
本來的流程是某房間 a飛機(jī)位置發(fā)生變化->服務(wù)器 服務(wù)器同時(shí)下發(fā)給a,b a飛機(jī)的位置;
現(xiàn)在因?yàn)檫@樣的方式不太好,比如延時(shí),比如碰撞等都會產(chǎn)生問題;
所以改成:
a飛機(jī)位置發(fā)生變化->服務(wù)器 ,存儲位置
服務(wù)器定時(shí)(1/60 s)下發(fā)給a,b 各自的最新位置;
這樣的話碰撞什么的都可以在服務(wù)端計(jì)算,....
然后我現(xiàn)在的解決辦法(都是在群里各位大大給的,感謝)
a飛機(jī)位置發(fā)生變化->服務(wù)器 ,存儲位置到memcache;
新開一個(gè)worker去定時(shí)讀取memcache的內(nèi)容,凡是房間內(nèi)有2個(gè)人的,下發(fā)位置給他們;
相關(guān)代碼:
use \Workerman\Worker;
use \GatewayWorker\Lib\Gateway;
use \GatewayWorker\Lib\Store;
use \Workerman\Autoloader;
// 自動(dòng)加載類
require_once __DIR__ . '/../../Workerman/Autoloader.php';
Autoloader::setRootPath(__DIR__);
$task = new Worker();
// 開啟多少個(gè)進(jìn)程運(yùn)行定時(shí)任務(wù),注意多進(jìn)程并發(fā)問題
$task->count = 1;
$task->onWorkerStart = function($task)
{
$time_interval = 0.0166;
$time_interval = 2.5;
\Workerman\Lib\Timer::add($time_interval, function()
{
echo "task run\n";
//Gateway::
//Gateway::sendToClient(1, '3333');
$all_room_key = "ALL_ROOM";
$room_key_pre = "ROOM_";
$store = Store::instance('room');
$all_room = $store->get($all_room_key);
if ( $all_room )
{
foreach ( $all_room as $kk => $vv )
{
$room_id = $kk;
$room_info = $store->get($room_key_pre.$room_id);
if ( count($room_info) > 1 )
{
//{"d":{"y":68,"x":477,"uid":7,"timestamp":54.353},"cmd":"updatePosition"}
$time = explode ( " ", microtime () );
$timestamp = $time +$time ;
foreach ( $room_info as $kk2 => $vv2 )
{
Gateway::sendToClient($kk, '{"d":{"y":'.$vv2.',"x":'.$vv2.',"uid":'.$vv2.',"timestamp":'.$timestamp.'},"cmd":"updatePosition"}');
}
}
}
}
});
};
然后這樣的話因?yàn)椴皇莗hp內(nèi)存直接處理, 如果客戶端連接多,memcache壓力估計(jì)非常大,求各位大大...
使用memcache不合理
每個(gè)房間每1/60秒讀一次memcache,那么1000個(gè)房間每秒讀取memcache就要讀取60000次memcache,單臺memcache服務(wù)器是很難支持這么高的訪問量的。而且這樣的設(shè)計(jì)也不合理。
基于Workerman做
坐標(biāo)從php內(nèi)存變量里面讀是最快的,實(shí)際上不推薦所有游戲都去用gatewayWorker,直接基于workerman來做更靈活,更穩(wěn)定,更高性能。例如下面的demo,是直接設(shè)置/讀取php變量,性能更高,更穩(wěn)定,擴(kuò)展性更好。
<?php
use Workerman\Worker;
use Workerman\Lib\Timer;
require_once './Workerman/Autoloader.php';
// 初始化一個(gè)worker容器
$worker = new Worker('Text://0.0.0.0:6001');
// 全局對象,保存當(dāng)前進(jìn)程內(nèi)的房間數(shù)據(jù),每個(gè)房間兩個(gè)玩家(連接對象)
// 格式[room_id1=>, room_id2=>
$rooms = array();
// 全局變量。保存當(dāng)前的連接對象,方便在任意函數(shù)中獲得當(dāng)前連接對象
$current_connection = null;
// 固定為1
$worker->count = 1;
// tcp連接建立時(shí),初始化坐標(biāo)
$worker->onConnect = function($connection)
{
$connection->x = 0;
$connection->y = 0;
// 發(fā)送當(dāng)前連接的id
$connection->send('{"type":"connection_id", "id":"'.$connection->id.'"}');
};
// 當(dāng)有客戶端發(fā)來消息時(shí)執(zhí)行的回調(diào)函數(shù)
$worker->onMessage = function($connection, $data)
{
// 全局保存當(dāng)前對象,方便給當(dāng)前連接發(fā)送數(shù)據(jù)
global $current_connection;
$current_connection = $connection;
// 客戶端傳遞的數(shù)據(jù)格式類似
// {"mod":"Room", "act":"join", "args":{"room_id":13}}
$data = json_decode($data, true);
$class = $data;
$method = $data;
$callback = array($class, $method);
// 執(zhí)行某個(gè)類的某個(gè)方法
if(is_callable($callback))
{
call_user_func_array($callback, $data);
}
else
{
$connection->send('{"type":"err", "msg":"invalid packet"}');
}
};
// 當(dāng)有客戶端連接斷開時(shí)
$worker->onClose = function($connection)
{
global $rooms;
$room_id = isset($connection->room_id) ? $connection->room_id : null;
// 還沒加入房間
if($room_id === null)
{
return;
}
// 清理房間信息
unset($rooms);
// 如果有定時(shí)器,清理定時(shí)器
$timer_id = isset($rooms) ? $rooms : 0;
if($timer_id)
{
Timer::del($timer_id);
unset($rooms);
}
if(empty($rooms)) {
unset($rooms);
}
// 廣播退出事件
Room::broadcast($room_id, array("type"=>"logout", "id"=>$connection->id));
};
// 運(yùn)行所有的worker(其實(shí)當(dāng)前只定義了一個(gè))
Worker::runAll();
// 坐標(biāo)類
class Location
{
public static function update($x, $y)
{
global $current_connection;
$current_connection->x = $x;
$current_connection->y = $y;
}
}
// 房間類
class Room
{
// 加入房間
// {"mod":"Room", "act":"join", "args":{"room_id":13}}
public static function join($room_id)
{
global $rooms, $current_connection;
$players = self::getPlayers($room_id);
$player_count = count($players);
// 房間已經(jīng)滿了
if($player_count === 2)
{
return $current_connection->send('{"type":"err", "msg":"room full"}');
}
// 加入房間
$rooms = $current_connection;
// 用一個(gè)臨時(shí)屬性room_id存儲當(dāng)前連接的房間號
$current_connection->room_id = $room_id;
// 已經(jīng)兩人,開始戰(zhàn)斗
if($player_count+1 === 2)
{
// 發(fā)個(gè)包給客戶端,開始戰(zhàn)斗
self::broadcastBeginFight($room_id);
// 建立一個(gè)定時(shí)器發(fā)送當(dāng)前房間($room_id)玩家的坐標(biāo)
$rooms = Timer::add(1, 'Room::broadcastLocation', array($room_id));
var_export($rooms);
}
}
// 廣播開始戰(zhàn)斗
public static function broadcastBeginFight($room_id)
{
$data = array(
'type' => 'begin_fight'
);
self::broadcast($room_id, $data);
}
// 廣播坐標(biāo)
public static function broadcastLocation($room_id)
{
list($player1_connection, $player2_connection) = array_values(self::getPlayers($room_id));
$location_data = array(
'type' => 'location',
'data' => array(
array($player1_connection->id, $player1_connection->x, $player1_connection->y),
array($player2_connection->id, $player2_connection->x, $player2_connection->y)
)
);
self::broadcast($room_id, $location_data);
}
// 向某個(gè)room廣播
public static function broadcast($room_id, array $data)
{
$data_buffer = json_encode($data);
foreach(self::getPlayers($room_id) as $connection)
{
$connection->send($data_buffer);
}
}
// 獲得某個(gè)room的玩家連接對象
protected static function getPlayers($room_id)
{
global $rooms;
if(!isset($rooms))
{
return array();
}
$connections = $rooms;
return $connections;
}
}
以上代碼親測ok
多進(jìn)程
上面的demo是單進(jìn)程的,并且只能設(shè)置成單進(jìn)程,目的是為了讓同一個(gè)房間的用戶都在一個(gè)進(jìn)程里面,方便共享坐標(biāo)等數(shù)據(jù)。
多進(jìn)程的方法就是啟動(dòng)多個(gè)上面的實(shí)例,每個(gè)實(shí)例一個(gè)端口,客戶端根據(jù)需要選擇連哪個(gè)服務(wù)器。
例如
1、每個(gè)實(shí)例作為一個(gè)區(qū),每個(gè)區(qū)多個(gè)房間。用戶選擇某個(gè)區(qū)的某個(gè)房間進(jìn)入
2、也可以把端口號+roomid作為房間號??梢愿鶕?jù)房間號得到端口號和實(shí)際的room_id
分布式部署
由于每個(gè)實(shí)例都是獨(dú)立的,完全可以部署在不同的服務(wù)器上,組成一個(gè)集群。
房間號規(guī)則可以為 ip+port+room_id
擴(kuò)展閱讀
單個(gè)進(jìn)程內(nèi)如果有阻塞操作,比如讀數(shù)據(jù)庫/redis等存儲,會導(dǎo)致進(jìn)程阻塞,解決方法是建議一些任務(wù)進(jìn)程,處理阻塞任務(wù),然后異步通知游戲進(jìn)程
參考 http://wenda.workerman.net/?/question/358
上面demo測試方法
demo使用的是Text協(xié)議,可以改成其它協(xié)議如websocket。
Text協(xié)議測試方法:
telnet 127.0.0.1 6001
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
{"type":"connection_id", "id":"2"}
{"mod":"Room", "act":"join", "args":{"room_id":13}}
{"type":"begin_fight"}
{"type":"location","data":}
{"type":"location","data":}
{"mod":"Location", "act":"update", "args":{"x":13, "y":56}}
{"type":"location","data":}
{"type":"location","data":}
{"type":"location","data":}
{"type":"location","data":}
{"type":"location","data":}
{"type":"location","data":}
{"type":"location","data":}
{"type":"location","data":}
說明:
加入房間的包
{"mod":"Room", "act":"join", "args":{"room_id":13}}
更新坐標(biāo)的包
{"mod":"Location", "act":"update", "args":{"x":13, "y":56}}