MMO游戏技能攻击区域的计算2--给地图划分格子

来源:互联网 发布:云计算阅读理解及答案 编辑:程序博客网 时间:2024/05/01 00:08

本文来自肥宝游戏,引用必须注明出处!

之前已经写过关于攻击区域的算法。但后来,发现别人的游戏(《凡人修真2》其实也不算别人的游戏了)不是这么写的。我居然找不到算矩形之类的代码。找来找去,发现实现思路跟我的差别太大。我的思路在这里

先说一种情况,就是一个玩家的角色在地图上走动的时候,是需要不断的跟服务端同步的。


例如图中坐标系,从位置1走到位置2.这个过程中是要不断的跟服务端同步的。服务端也会不断地跟其他客户端同步

在玩家的角度来看,角色A是从位置1到位置2匀速运动。而在同一个屏幕下的其他玩家也是在匀速运动。这是正常情况下,也是理想的情况。

但客户端不可能没移动一个像素就跟告诉服务端一下,我走了一个点。

这个是不现实的,首先没有必要,移动一个像素,客户端可能根本没什么变化。

第二,假如屏幕上有10个人,一个角色移动了,服务端就要广播给10个人,在一瞬间就是发送了10个消息。假如这10个人都在动,就是同时发送10*10个消息

假如屏幕上有100个人同时在动,就是100*100,假如是1000,就是1000*1000了。我相信这个游戏基本就只能在地图上跑,其他什么都做不了了,甚至跑都跑不动。

所以可以得出结论:客户端跟服务端必须不是同步的,以减少通信。这样的话就只能走一段距离才同步一下位置。其他客户端收到这个角色在动,就自己做一个匀速运动过去。

对于客户端来说,角色移动依然是上图的样子,对服务端来说是怎样的呢?


对服务端来说,没收到客户端的一个请求,就改变一下位置,所以从位置1到位置2,是按着图中换色路线闪过去的。

其他客户端收到同步的消息,就做匀速运动,然角色看起来好像在一直走着。

那么现在问题来了,客户端跟服务端同步的标准是什么呢?什么情况下需要同步一下?

1.按时间同步

每隔一段时间同步一下,例如100毫秒。我们的服务端的怪是这样同步的,因为每隔一段时间需要执行一下AI处理,是否遇到玩家,是否需要追击,是否需要释放技能等等。这些都要做,所以移动的话,也就放在这个模块里面了。我不知道会不会有的游戏客户端会这样处理。

2.按空间同步

每移动一段距离,就同步一下。但是这个距离怎么算呢?是从上一个点开始算起么?我们没这么算过,可能等我客户端的时候可以这么试试。实际上我们是把地图画了一个个格子。就像上图那样,假如地图是1000*1000的。我每100个像素划分一个格子。就可以得出X轴和Y轴都分成10份了。角色只有从一个格子移动到另外一个格子的情况下,才通知服务端。

这样做的好处是大大缩减了通信的数量。当然也不是没有代价的,如果卡的时候,就会看到角色从位置1走到位置1.1,然后定住了,过几秒钟,它可能出现在位置2了。如果是在多人同屏夺旗之类(独步天下的帮派战),真是日了狗了,杀过去后发现玩家居然闪过去了。

上面是地图的处理,总结一下:

无论服务端是把地图分成一个个格子,在同一个格子内,是算同一点的,如果你把格子画得很大很大,例如整个地图就是一格子,那么在服务端看来,所有角色都是堆在同一个位置上面了。

那么格子需要多大呢?

问了一下策划,他们回答:肯定是越小越好啦,这样就能越精确。

问了一下客户端,说:他们没有格子的概念的,服务端分的格子,他们基本上只是判断一下消息发送的时机而已。

对服务端来说,虽然不可能整个地图就一个格子,但是也是想越大越好啊。

顺便一说,地图主要是客户端的天下,地图格子是服务端用来做技能处理的。

对于一般的2d游戏来说,格子大小最好还是先设定好一个值,然后看实际效果,再慢慢调。因为每个游戏的场景、建筑、任务的大小都不同,尤其是像大战神这种可以放大缩小的游戏,更加需要直接看游戏来作调整。一切以实际感官为标准,然后尽量画大一点。


=================================================================================

我们先把格子分好!!!无论用哪个计算方式,都需要先把格子分好。按每个格子CellBase大小为20像素,当然实际应用中可以长宽不同。如下面就是一个220*220的地图,分成11*11个格子。格子中的数字就是其坐标,格子的坐标。



明确好格子的概念,就可以开始思考新的技能攻击区域了。

先定义好一些变量,

DataManage.h 存放一些测试用的数据,数据的值不是很重要,不能太小,否则全部在一个点上面就没意思了。也不能太大,一个技能把整个地图包含进去也没啥意思的。

        //数据管理,数据来源应该是来自于配置文件以及游戏运行过程的实际数据,这里是为了方便管理    class CDataManager    {    private:        CDataManager();        ~CDataManager();        void init();    public:        static CDataManager* instance();//单例                //======技能数据,这个应该来自配置文件,由策划配出技能实际要求==============        int skillDistance;//技能释放距离                int rectWidth;//矩形攻击区域的宽度        int rectHeight;//矩形攻击区域的高度                int angleBeta;//扇形攻击区域的角度        int rBeta;//扇形攻击区域的半径                //======角色站位数据,必须来自实际的运算,玩家是不断走动的=================        SPoint attackerPoint;//攻击者位置        SPoint defenserPoint;//被攻击者位置或技能释放点        SeqSPoint otherRoles;//其他需要检测的角色    }; 


一般情况下攻击区域分为以下几种:

1.点对点,对个人进行攻击

点对点,其实就是判断两个角色所在的格子之间的长度是不是在所配置的长度之内了。具体函数如下

//判断两点间是否超过一定距离bool CMapManager::isFarThanDistance(SPoint a, SPoint b, int distance){    //求出相对距离xy    int x = (a.x - b.x) * CellBase;//坐标点都是格子的坐标点,所以要乘以格子的长度    int y = (a.y - b.y) * CellBase;        if(x * x + y * y > distance *distance) return true;//超过距离(勾股定理)    return false;//未超过}

2.射线攻击,其实就是矩形区域


//判断一个点是否在矩形内,这个要求与坐标轴平行bool CMapManager::inRect( double minx, double miny, double maxx, double maxy, SPoint p){    //判断点p的xy是否在矩形上下左右之间    if(p.x >= minx && p.x <= maxx && p.y >= miny && p.y <= maxy) return true;    return false;}//计算两点之间的距离double CMapManager::computeDistance(SPoint& from, SPoint& to){    return (sqrt(pow(to.x - from.x, 2) + pow(to.y - from.y, 2)))* CellBase;}//直角坐标--绝对坐标转相对坐标SPoint CMapManager::changeAbsolute2Relative(                                            SPoint originPoint,//相对坐标系的原点                                            SPoint directionPoint,//指向x轴方向的点                                            SPoint changePoint)//需要转换的坐标{    //originPoint为图中A点,directionPoint为图中B点,changePoint为图中C点    SPoint rePoint;    if (originPoint.x == directionPoint.x && originPoint.y == directionPoint.y)//方向点跟原点重合,就用平行于原坐标的x轴来算就行了    {//AB点重合,方向指向哪里都没所谓,肯定按原来的做方便        rePoint.x = changePoint.x - originPoint.x;        rePoint.y = changePoint.y - originPoint.y;    }    else    {        //计算三条边        double a = computeDistance(directionPoint, changePoint);        double b = computeDistance(changePoint, originPoint);        double c = computeDistance(directionPoint, originPoint);                double cosA = (b*b + c*c - a*a) / 2*b*c;//余弦        rePoint.x = a * cosA / CellBase;//相对坐标x        rePoint.y = sqrt(a*a - a * cosA * a * cosA) / CellBase;//相对坐标y    }    return rePoint;}bool CMapManager::inRectRelat( SPoint originPoint, SPoint directionPoint, SPoint checkPoint){    //检测每一个角色是否在矩形内。    SPoint rePoint = changeAbsolute2Relative(originPoint, directionPoint, checkPoint);//相对坐标    //skillWidth为图中宽度,skillLong为图中长度    int skillWidth = CDataManager::instance()->rectWidth/CellBase;//矩形攻击区域的宽度    int skillLong = CDataManager::instance()->rectHeight/CellBase;//矩形攻击区域的高度    //宽度是被AB平分的,从A点开始延伸长度    return inRect(0, - skillWidth/2, skillLong, skillWidth/2, rePoint);//相对坐标下攻击范围}SPoint changeAbsolute2Relative(SPoint originPoint, SPoint changePoint){    SPoint rePoint;    rePoint.x = changePoint.x - originPoint.x;    rePoint.y = changePoint.y - originPoint.y;    return rePoint;}

3.扇形攻击

bool checkInFan( SPoint originPoint, SPoint directionPoint, SPoint checkPoint ){    //先求主目标的单位向量    SPoint rePoint = changeAbsolute2Relative(originPoint, directionPoint);//攻击者与主目标的向量    double longB = sqrt(rePoint.x * rePoint.x + rePoint.y * rePoint.y) ;//长度    rePoint.x /= longB;    rePoint.y /= longB;//求单位向量        //然后求出检测点的向量    SPoint rePointC = changeAbsolute2Relative(originPoint, checkPoint);//图中C点相对坐标    double longC = sqrt(rePointC.x * rePointC.x + rePointC.y * rePointC.y);//长度    rePointC.x /= longC;    rePointC.y /= longC;//求单位向量        //根据向量的点击来求角度    double jiaodu = acos(rePoint.x * rePointC.x + rePoint.y * rePointC.y) * 180 /PI;//实际的角度大小    double angleBeta = CDataManager::instance()->angleBeta;        if ( jiaodu < angleBeta)    {//相差的角度小于配置的角度,所以受到攻击。要注意,这里的角度都是在0°到360°之间        return true;//在角度范围内    }    return false;}

4.圆形攻击

圆形攻击其实就是跟点对点的攻击计算一样的。

//判断两点间是否超过一定距离bool CMapManager::isFarThanDistance(SPoint a, SPoint b, int distance){    //求出相对距离xy    int x = (a.x - b.x) * CellBase;//坐标点都是格子的坐标点,所以要乘以格子的长度    int y = (a.y - b.y) * CellBase;        if(x * x + y * y > distance *distance) return true;//超过距离(勾股定理)    return false;//未超过}


======================================================

上面的计算其实跟上一篇文章技能攻击区域的计算差不多。只是加了个格子。但是基本算法思想完全没有变化。

但是加了格子后,就多了一种计算方法。

我们看一下不同技能在区域在格子上面是怎么表示的。

1.点对点,没变化,就不说了。

2.扇形,如果是小的扇形,其实就是自己所在的那个格子,就判断玩家是否在同一个点就行了。如果扇形很大。


图中使用的是相对位置,以攻击者为圆心,这个只考虑了一个朝向。

3.矩形,其实就是一条射线


4,再看看圆形:


那么怎么算呢?

这些点都是通过配置来实现的。

SPoint p;    p.x = 0; p.y = 0;    cellsFan.push_back(p);    p.x = -1; p.y = 1;    cellsFan.push_back(p);    p.x = 0; p.y = 1;    cellsFan.push_back(p);    p.x = 1; p.y = 1;    cellsFan.push_back(p);    p.x = -2; p.y = 2;    cellsFan.push_back(p);    p.x = -1; p.y = 2;    cellsFan.push_back(p);    p.x = 0; p.y = 2;    cellsFan.push_back(p);    p.x = 1; p.y = 2;    cellsFan.push_back(p);    p.x = 2; p.y = 2;    cellsFan.push_back(p);
这是扇形的配置。可以想象到,这种配置是跟图形无关的,几乎所有工作了都在策划身上,就看策划怎么配了。


==========================================================================

上面的代码很碎,下面黏贴所有代码。由于文章太长了,下一章再进行效率对比了。

========DataManager.h===========

////  ConfigManager.h//  HelloWorld//  关注微信公众号:传说之路,大家共同学习//  Created by feiyin001 on 16/4/3.//  Copyright (c) 2016年 FableGame. All rights reserved.//#ifndef __HelloWorld__ConfigManager__#define __HelloWorld__ConfigManager__#include <stdio.h>#include "MapManager.h"namespace FableGame {            //数据管理,数据来源应该是来自于配置文件以及游戏运行过程的实际数据,这里是为了方便管理    class CDataManager    {    private:        CDataManager();        ~CDataManager();        void init();    public:        static CDataManager* instance();//单例                //======技能数据,这个应该来自配置文件,由策划配出技能实际要求==============        int skillDistance;//技能释放距离                int rectWidth;//矩形攻击区域的宽度        int rectHeight;//矩形攻击区域的高度                int angleBeta;//扇形攻击区域的角度        int rBeta;//扇形攻击区域的半径                //======格子区域的配置===========        SeqSPoint cellsFan;//扇形区域        SeqSPoint cellsRect;//矩形        SeqSPoint cellsCyc;//圆形                        //======角色站位数据,必须来自实际的运算,玩家是不断走动的=================        SPoint attackerPoint;//攻击者位置        SPoint defenserPoint;//被攻击者位置或技能释放点        SeqSPoint otherRoles;//其他需要检测的角色    };}#endif /* defined(__HelloWorld__ConfigManager__) */
=========DataManager.cpp=============

////  ConfigManager.cpp//  HelloWorld//  关注微信公众号:传说之路,大家共同学习//  Created by feiyin001 on 16/4/3.//  Copyright (c) 2016年 FableGame. All rights reserved.//#include "DataManager.h"using namespace FableGame;CDataManager::CDataManager(){    init();}CDataManager::~CDataManager(){    }CDataManager* CDataManager::instance(){    static CDataManager _instance;    return &_instance;}void CDataManager::init(){    //数据的具体数值不是很重要,重要的是不要太大或者太小,导致技能效果全在一个点上面或者包含整个地图。    //算法是一样的,关键是看计算时间    //=================技能相关配置==============    skillDistance = 50;//技能释放距离,像素        rectWidth = 50;//矩形区域的宽度,像素    rectHeight = 20;//矩形区域的高度,像素        angleBeta = 60;//扇形攻击区域的角度    rBeta = 30;//扇形攻击区域的半径,像素        //=================通过配置的格子的方式来做技能===    SPoint p;    //扇形    p.x = 0; p.y = 0; cellsFan.push_back(p);    p.x = -1; p.y = 1; cellsFan.push_back(p);    p.x = 0; p.y = 1; cellsFan.push_back(p);    p.x = 1; p.y = 1; cellsFan.push_back(p);    p.x = -2; p.y = 2; cellsFan.push_back(p);    p.x = -1; p.y = 2; cellsFan.push_back(p);    p.x = 0; p.y = 2; cellsFan.push_back(p);    p.x = 1; p.y = 2; cellsFan.push_back(p);    p.x = 2; p.y = 2; cellsFan.push_back(p);    //矩形    p.x = 0; p.y = 0; cellsRect.push_back(p);    p.x = 0; p.y = 1; cellsRect.push_back(p);    p.x = 1; p.y = 1; cellsRect.push_back(p);    p.x = 1; p.y = 2; cellsRect.push_back(p);    p.x = 2; p.y = 2; cellsRect.push_back(p);    p.x = 2; p.y = 3; cellsRect.push_back(p);    p.x = 3; p.y = 3; cellsRect.push_back(p);    p.x = 3; p.y = 4; cellsRect.push_back(p);        //圆形    p.x = -1; p.y = 2; cellsCyc.push_back(p);    p.x = 0; p.y = 2; cellsCyc.push_back(p);    p.x = 1; p.y = 2; cellsCyc.push_back(p);    p.x = -2; p.y = 1; cellsCyc.push_back(p);    p.x = -1; p.y = 1; cellsCyc.push_back(p);    p.x = 0; p.y = 1; cellsCyc.push_back(p);    p.x = 1; p.y = 1; cellsCyc.push_back(p);    p.x = 2; p.y = 1; cellsCyc.push_back(p);    p.x = -2; p.y = 0; cellsCyc.push_back(p);    p.x = -1; p.y = 0; cellsCyc.push_back(p);    p.x = 0; p.y = 0; cellsCyc.push_back(p);    p.x = 1; p.y = 0; cellsCyc.push_back(p);    p.x = 2; p.y = 0; cellsCyc.push_back(p);    p.x = -2; p.y = -1; cellsCyc.push_back(p);    p.x = -1; p.y = -1; cellsCyc.push_back(p);    p.x = 0; p.y = -1; cellsCyc.push_back(p);    p.x = 1; p.y = -1; cellsCyc.push_back(p);    p.x = 2; p.y = -1; cellsCyc.push_back(p);    p.x = -1; p.y = -2; cellsCyc.push_back(p);    p.x = 0; p.y = -2; cellsCyc.push_back(p);    p.x = 1; p.y = -2; cellsCyc.push_back(p);        //=================角色实际站位==============    attackerPoint.x = 0;//攻击者位置    attackerPoint.y = 1;//攻击者位置        defenserPoint.x = 8;//被攻击者位置或技能释放点    defenserPoint.y = 8;//被攻击者位置或技能释放点        //其他角色位置,为了方便测试,在每个格子都放一个人吧。    //otherRoles其他需要检测的角色    for (int i = 0; i <= 100; i++) {        for (int j = 0; j <= 100; j++) {            SPoint p;            p.x = i;            p.y = j;            otherRoles.push_back(p);        }    }  }
==========MapManager.h===============

////  Header.h//  HelloWorld//  关注微信公众号:传说之路,大家共同学习//  Created by feiyin001 on 16/4/3.//  Copyright (c) 2016年 FableGame. All rights reserved.//#ifndef HelloWorld_Header_h#define HelloWorld_Header_h#include <vector>#define PI 3.1412;//圆周率#define CellBase 20;//格子的大小namespace FableGame {        struct SPoint    {        int x;        int y;    };    typedef std::vector<SPoint> SeqSPoint;        //地图上各种处理都放在这里    class CMapManager    {                //判断两点间是否超过一定距离        bool isFarThanDistance(SPoint a, SPoint b, int distance);                //判断一个点是否在矩形内,这个要求与坐标轴平行        bool inRect( double minx, double miny, double maxx, double maxy, SPoint p);        //判断一个点是否在矩形内,        bool inRectRelat( SPoint originPoint, SPoint directionPoint, SPoint checkPoint);        //判断是否在扇形内        bool checkInFan( SPoint originPoint, SPoint directionPoint, SPoint checkPoint );                        //计算两点之间的距离        double computeDistance(SPoint& from, SPoint& to);                /**         * 直角坐标--绝对坐标转相对坐标         * originPoint 相对坐标系的原点         * directionPoint 指向x轴方向的点         * changePoint 需要转换的坐标         */        SPoint changeAbsolute2Relative(SPoint originPoint, SPoint directionPoint, SPoint changePoint);        //这个转换的坐标轴是跟原来的平行的        SPoint changeAbsolute2Relative(SPoint originPoint, SPoint changePoint);                        //======检测是否在格子配置的图形里面======        bool checkInCellFan(SPoint originPoint, SPoint checkPoint);        bool checkInCellRect(SPoint originPoint, SPoint checkPoint);        bool checkInCellCyc(SPoint originPoint, SPoint checkPoint);            }; }#endif
===========MapManager.cpp================

////  MapManager.cpp//  HelloWorld//  关注微信公众号:传说之路,大家共同学习//  Created by feiyin001 on 16/4/3.//  Copyright (c) 2016年 FableGame. All rights reserved.//#include "MapManager.h"#include <math.h>#include "DataManager.h"using namespace FableGame;//判断两点间是否超过一定距离bool CMapManager::isFarThanDistance(SPoint a, SPoint b, int distance){    //求出相对距离xy    int x = (a.x - b.x) * CellBase;//坐标点都是格子的坐标点,所以要乘以格子的长度    int y = (a.y - b.y) * CellBase;        if(x * x + y * y > distance *distance) return true;//超过距离(勾股定理)    return false;//未超过}//判断一个点是否在矩形内,这个要求与坐标轴平行bool CMapManager::inRect( double minx, double miny, double maxx, double maxy, SPoint p){    //判断点p的xy是否在矩形上下左右之间    if(p.x >= minx && p.x <= maxx && p.y >= miny && p.y <= maxy) return true;    return false;}//计算两点之间的距离double CMapManager::computeDistance(SPoint& from, SPoint& to){    return (sqrt(pow(to.x - from.x, 2) + pow(to.y - from.y, 2)))* CellBase;}//直角坐标--绝对坐标转相对坐标SPoint CMapManager::changeAbsolute2Relative(                                            SPoint originPoint,//相对坐标系的原点                                            SPoint directionPoint,//指向x轴方向的点                                            SPoint changePoint)//需要转换的坐标{    //originPoint为图中A点,directionPoint为图中B点,changePoint为图中C点    SPoint rePoint;    if (originPoint.x == directionPoint.x && originPoint.y == directionPoint.y)//方向点跟原点重合,就用平行于原坐标的x轴来算就行了    {//AB点重合,方向指向哪里都没所谓,肯定按原来的做方便        rePoint.x = changePoint.x - originPoint.x;        rePoint.y = changePoint.y - originPoint.y;    }    else    {        //计算三条边        double a = computeDistance(directionPoint, changePoint);        double b = computeDistance(changePoint, originPoint);        double c = computeDistance(directionPoint, originPoint);                double cosA = (b*b + c*c - a*a) / 2*b*c;//余弦        rePoint.x = a * cosA / CellBase;//相对坐标x        rePoint.y = sqrt(a*a - a * cosA * a * cosA) / CellBase;//相对坐标y    }    return rePoint;}bool CMapManager::inRectRelat( SPoint originPoint, SPoint directionPoint, SPoint checkPoint){    //检测每一个角色是否在矩形内。    SPoint rePoint = changeAbsolute2Relative(originPoint, directionPoint, checkPoint);//相对坐标    //skillWidth为图中宽度,skillLong为图中长度    int skillWidth = CDataManager::instance()->rectWidth/CellBase;//矩形攻击区域的宽度    int skillLong = CDataManager::instance()->rectHeight/CellBase;//矩形攻击区域的高度    //宽度是被AB平分的,从A点开始延伸长度    return inRect(0, - skillWidth/2, skillLong, skillWidth/2, rePoint);//相对坐标下攻击范围}SPoint changeAbsolute2Relative(SPoint originPoint, SPoint changePoint){    SPoint rePoint;    rePoint.x = changePoint.x - originPoint.x;    rePoint.y = changePoint.y - originPoint.y;    return rePoint;}bool checkInFan( SPoint originPoint, SPoint directionPoint, SPoint checkPoint ){    //先求主目标的单位向量    SPoint rePoint = changeAbsolute2Relative(originPoint, directionPoint);//攻击者与主目标的向量    double longB = sqrt(rePoint.x * rePoint.x + rePoint.y * rePoint.y) ;//长度    rePoint.x /= longB;    rePoint.y /= longB;//求单位向量        //然后求出检测点的向量    SPoint rePointC = changeAbsolute2Relative(originPoint, checkPoint);//图中C点相对坐标    double longC = sqrt(rePointC.x * rePointC.x + rePointC.y * rePointC.y);//长度    rePointC.x /= longC;    rePointC.y /= longC;//求单位向量        //根据向量的点击来求角度    double jiaodu = acos(rePoint.x * rePointC.x + rePoint.y * rePointC.y) * 180 /PI;//实际的角度大小    double angleBeta = CDataManager::instance()->angleBeta;        if ( jiaodu < angleBeta)    {//相差的角度小于配置的角度,所以受到攻击。要注意,这里的角度都是在0°到360°之间        return true;//在角度范围内    }    return false;}bool checkInCellFan(SPoint originPoint, SPoint checkPoint){    SPoint rePoint = changeAbsolute2Relative(originPoint, checkPoint);//计算出相对位置    //判断是否跟配置某一点相同    for (SeqSPoint::iterator iter = CDataManager::instance()->cellsFan.begin();         iter != CDataManager::instance()->cellsFan.end() ;         iter++)    {        if (iter->x == rePoint.x && iter->y == rePoint.y) {            return true;        }    }    return false;}bool checkInCellRect(SPoint originPoint, SPoint checkPoint){    SPoint rePoint = changeAbsolute2Relative(originPoint, checkPoint);    for (SeqSPoint::iterator iter = CDataManager::instance()->cellsRect.begin();         iter != CDataManager::instance()->cellsRect.end() ;         iter++)    {        if (iter->x == rePoint.x && iter->y == rePoint.y) {            return true;        }    }    return false;}bool checkInCellCyc(SPoint originPoint, SPoint checkPoint){    SPoint rePoint = changeAbsolute2Relative(originPoint, checkPoint);    for (SeqSPoint::iterator iter = CDataManager::instance()->cellsCyc.begin();         iter != CDataManager::instance()->cellsCyc.end() ;         iter++)    {        if (iter->x == rePoint.x && iter->y == rePoint.y) {            return true;        }    }    return false;}











1 0
原创粉丝点击