实时战斗游戏打飞机定时发送坐标问题

ketle

需求:
一个手机打飞机游戏,一个房间2个飞机,然后各自控制飞机位置,子弹自动发射,看谁先挂;

本来是下载了win版聊天室框架GatewayWorker来搞,正好都合适 ,有房间,有各种存储,搞起来很顺;
本来的流程是某房间 a飞机位置发生变化->服务器 服务器同时下发给a,b a飞机的位置;
现在因为这样的方式不太好,比如延时,比如碰撞等都会产生问题;
所以改成:

a飞机位置发生变化->服务器 ,存储位置
服务器定时(1/60 s)下发给a,b 各自的最新位置;
这样的话碰撞什么的都可以在服务端计算,....

然后我现在的解决办法(都是在群里各位大大给的,感谢)

a飞机位置发生变化->服务器 ,存储位置到memcache;
新开一个worker去定时读取memcache的内容,凡是房间内有2个人的,下发位置给他们;
相关代码:

use \Workerman\Worker; 
use \GatewayWorker\Lib\Gateway;
use \GatewayWorker\Lib\Store; 
use \Workerman\Autoloader;
// 自动加载类
require_once __DIR__ . '/../../Workerman/Autoloader.php';
Autoloader::setRootPath(__DIR__);

$task = new Worker();
// 开启多少个进程运行定时任务,注意多进程并发问题
$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"}');
                    }
                }
            }
        }

    });
};

然后这样的话因为不是php内存直接处理, 如果客户端连接多,memcache压力估计非常大,求各位大大...

8612 5 8
5个回答

walkor

使用memcache不合理
每个房间每1/60秒读一次memcache,那么1000个房间每秒读取memcache就要读取60000次memcache,单台memcache服务器是很难支持这么高的访问量的。而且这样的设计也不合理。

基于Workerman做
坐标从php内存变量里面读是最快的,实际上不推荐所有游戏都去用gatewayWorker,直接基于workerman来做更灵活,更稳定,更高性能。例如下面的demo,是直接设置/读取php变量,性能更高,更稳定,扩展性更好。

<?php
use Workerman\Worker;
use Workerman\Lib\Timer;
require_once './Workerman/Autoloader.php';
// 初始化一个worker容器
$worker = new Worker('Text://0.0.0.0:6001');
// 全局对象,保存当前进程内的房间数据,每个房间两个玩家(连接对象)
// 格式[room_id1=>, room_id2=>
$rooms = array();
// 全局变量。保存当前的连接对象,方便在任意函数中获得当前连接对象
$current_connection = null;
// 固定为1
$worker->count = 1;
// tcp连接建立时,初始化坐标
$worker->onConnect = function($connection)
{
    $connection->x = 0;
    $connection->y = 0;
    // 发送当前连接的id
    $connection->send('{"type":"connection_id", "id":"'.$connection->id.'"}');
};
// 当有客户端发来消息时执行的回调函数
$worker->onMessage = function($connection, $data)
{
    // 全局保存当前对象,方便给当前连接发送数据
    global $current_connection;
    $current_connection = $connection;
    // 客户端传递的数据格式类似 
    // {"mod":"Room", "act":"join", "args":{"room_id":13}}
    $data = json_decode($data, true);
    $class = $data;
    $method = $data;
    $callback = array($class, $method);
    // 执行某个类的某个方法
    if(is_callable($callback))
    {
        call_user_func_array($callback, $data);
    }
    else
    {
        $connection->send('{"type":"err", "msg":"invalid packet"}');
    }
};
// 当有客户端连接断开时
$worker->onClose = function($connection)
{
    global $rooms;
    $room_id = isset($connection->room_id) ? $connection->room_id : null;
    // 还没加入房间
    if($room_id === null)
    {
        return;
    }
    // 清理房间信息
    unset($rooms);
    // 如果有定时器,清理定时器
    $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));
};
// 运行所有的worker(其实当前只定义了一个)
Worker::runAll();

// 坐标类
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);
        // 房间已经满了
        if($player_count === 2)
        {
            return $current_connection->send('{"type":"err", "msg":"room full"}');
        }
        // 加入房间
        $rooms = $current_connection;
        // 用一个临时属性room_id存储当前连接的房间号
        $current_connection->room_id = $room_id;
        // 已经两人,开始战斗
        if($player_count+1 === 2)
        {
            // 发个包给客户端,开始战斗
            self::broadcastBeginFight($room_id);
            // 建立一个定时器发送当前房间($room_id)玩家的坐标
            $rooms = Timer::add(1, 'Room::broadcastLocation', array($room_id));
            var_export($rooms);
        }

    }
    // 广播开始战斗
    public static function broadcastBeginFight($room_id)
    {
        $data = array(
            'type' => 'begin_fight'
        );
        self::broadcast($room_id, $data);
    }
    // 广播坐标
    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);
    }
    // 向某个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);
        }
    }
    // 获得某个room的玩家连接对象
    protected static function getPlayers($room_id)
    {
        global $rooms;
        if(!isset($rooms))
        {
            return array();
        }
        $connections = $rooms;
        return $connections;
    }

}

以上代码亲测ok

多进程
上面的demo是单进程的,并且只能设置成单进程,目的是为了让同一个房间的用户都在一个进程里面,方便共享坐标等数据。

多进程的方法就是启动多个上面的实例,每个实例一个端口,客户端根据需要选择连哪个服务器。
例如
1、每个实例作为一个区,每个区多个房间。用户选择某个区的某个房间进入
2、也可以把端口号+roomid作为房间号。可以根据房间号得到端口号和实际的room_id

分布式部署
由于每个实例都是独立的,完全可以部署在不同的服务器上,组成一个集群。
房间号规则可以为 ip+port+room_id

扩展阅读
单个进程内如果有阻塞操作,比如读数据库/redis等存储,会导致进程阻塞,解决方法是建议一些任务进程,处理阻塞任务,然后异步通知游戏进程
参考 http://wenda.workerman.net/?/question/358

  • 暂无评论
walkor

上面demo测试方法
demo使用的是Text协议,可以改成其它协议如websocket。
Text协议测试方法:

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}}
更新坐标的包
{"mod":"Location", "act":"update", "args":{"x":13, "y":56}}

  • 暂无评论
ketle

谢谢 walkor 等下我试试

  • 暂无评论
wk617070321

多房间棋牌游戏好像也可以这样干

  • 暂无评论
muyu

举报:加入房间下标是palyers,删除客户端下表是players,到时导致运行时删不掉用户会一直在!

  • walkor 2017-10-10

    已经更正,感谢你的举报

年代过于久远,无法发表回答
🔝