游戏底层逻辑,运动&&寻路(四)

接着上次的来,我们在群体算法之前把基本的个体运动解决掉。

9、WallAvoidance避开墙壁

此处的墙被抽象为一条线段,不论你的游戏使用的是一条线段作为墙面的碰撞检测,或者用一个几何形状作为墙面,几何形状我们可以看作多条线段的集合,都可以用此方法。

墙类的实现

首先是线段类,作为基类,拥有几种几何计算的方法,便于计算平面线段的交点,不多说。

struct Seg
{
    Seg(Point p1, Point p2):
        _from(p1), _to(p2)
    {

    }
#define eps 1e-6
    //static math functions
    static int sgn(double x)
    {
        return x<-eps ? -1 : (x>eps);
    }

    static double Cross(const Point& p1, const Point& p2, const Point& p3, const Point& p4)
    {
        return (p2.x - p1.x)*(p4.y - p3.y) - (p2.y - p1.y)*(p4.x - p3.x);
    }

    static double Area(const Point& p1, const Point& p2, const Point& p3)
    {
        return Cross(p1, p2, p1, p3);
    }

    static double fArea(const Point& p1, const Point& p2, const Point& p3)
    {
        return fabs(Area(p1, p2, p3));
    }

    static bool Meet(const Point& p1, const Point& p2, const Point& p3, const Point& p4)
    {
        return max(min(p1.x, p2.x), min(p3.x, p4.x)) <= min(max(p1.x, p2.x), max(p3.x, p4.x))
            && max(min(p1.y, p2.y), min(p3.y, p4.y)) <= min(max(p1.y, p2.y), max(p3.y, p4.y))
            && sgn(Cross(p3, p2, p3, p4) * Cross(p3, p4, p3, p1)) >= 0
            && sgn(Cross(p1, p4, p1, p2) * Cross(p1, p2, p1, p3)) >= 0;
    }

    static Point Inter(const Point& p1, const Point& p2, const Point& p3, const Point& p4)
    {
        double s1 = fArea(p1, p2, p3), s2 = fArea(p1, p2, p4);
        return Point((p4.x*s1 + p3.x*s2) / (s1 + s2), (p4.y*s1 + p3.y*s2) / (s1 + s2));
    }

    static double PointToSegDist(double x, double y, double x1, double y1, double x2, double y2)
    {
        double cross = (x2 - x1) * (x - x1) + (y2 - y1) * (y - y1);
        if (cross <= 0) return sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));

        double d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
        if (cross >= d2) return sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));

        double r = cross / d2;
        double px = x1 + (x2 - x1) * r;
        double py = y1 + (y2 - y1) * r;
        return sqrt((x - px) * (x - px) + (py - y1) * (py - y1));
    }

    Point _from;
    Point _to;
};

墙类保护继承于线段类,我们并不把Seg类的几何计算方法暴露出来。

class Wall :public BaseEntity,protected Seg//inherite math functions as protected functions
{
public:
    Wall(Vec2, Vec2);
    virtual ~Wall();
public:
    virtual void update();
    virtual bool handleMsg(const Telegram&);
public:
    Vec2 from()const { return _from; }
    Vec2 to()const { return _to; }
    Vec2 normal()const
    {
        Vec2 normalLine(_to.x - _from.x, _from.x - _to.x);
        return (normalLine.getNormalized());
    }
    static bool lineIntersection(const Vec2 entPo, const Vec2 fleer, const Wall wall, double& entity2wall, Vec2& intersection);
};

bool Wall::lineIntersection(const Vec2 entPo, const Vec2 feeler, const Wall wall, double& entity2wall, Vec2& intersection)
{
    if (!Seg::Meet(entPo, feeler, wall.from(), wall.to()))
    {
        return false;
    }
    else
    {
        entity2wall = Seg::PointToSegDist(entPo.x, entPo.y, wall.from().x, wall.from().y, wall.to().x, wall.to().y);
        intersection = Seg::Inter(entPo, feeler, wall.from(), wall.to());
        return true;
    }
}

碰撞的避免

类似于上一篇,这次我们用“触须”来作为辅助图形

在猫科动物运动过程中,触须会帮助它们判断前方物体是否会碰撞,例如洞口能否钻过,此处仿真了这一点。

一般我们使用三根触须,触须从实体坐标出发,方向分别为沿朝向,然后两个45°角,沿朝向的长度为两侧的两倍。

触须会提前插入墙壁,产生的斥力作用于交点,斥力大小与插入深度成正比。

触须长度满足这样的规则:

类型一:永不碰撞规则

物体以最大速度运动,垂直于墙壁时,从触须接触墙壁开始,产生的斥力刚好让其在墙壁前停止运动。

m·v=∫f·dt 冲量的积分刚好等于其动量,此处f表示斥力的计算公式,在此处,f=插入墙壁深度。

类型二:允许碰撞

如果允许碰撞,表示你设定的最大速度超过了“安全速度”,我们需要使用基本刚体碰撞来避免其重叠(这步在游戏逻辑层的最后实现)。

触须

我们先来创建触须,添加成员变量:

std::vector<Vec2> _feelers;

double _wallDetectionFeelerLength;
void SteeringBehaviors::createFeelers()
{
    //feeler pointing straight in front
    _feelers[0] = _ownerVehicle->position() + _wallDetectionFeelerLength * _ownerVehicle->heading();

    //feeler to left
    Vec2 temp = _ownerVehicle->heading();
    //Vec2DRotateAroundOrigin(temp, halfPI * 3.5f);
    temp.rotate(Vec2::ZERO, halfPI*3.5f);
    _feelers[1] = _ownerVehicle->position() + _wallDetectionFeelerLength / 2.0f * temp;

    //feeler to right
    temp = _ownerVehicle->heading();
    temp.rotate(Vec2::ZERO, halfPI * 0.5f);
    _feelers[2] = _ownerVehicle->position() + _wallDetectionFeelerLength / 2.0f * temp;
}

其实就是设定好大小和角度而已。

紧接着我们开始计算_wallDetectionFeelerLength

在总时间t内,动量等于冲量的积分:

m.Vmax=∫t0f(t)dt(1)

在任意时刻t0,速度等于初始速度减加速度的积分:

Vt0=Vmax?∫t00f(t)/mdt(2)

作用力的表达式(此处定义一个值来配平单位:Da=1m/s3 ):

f(t)=dis=∫t00Vt0dt(3)

长度的表达式:

L=∫t0Vt0dt(4)

解出触须长度的表达式即可。

然后我们根据想要实现的效果

(1)允许碰撞:_wallDetectionFeelerLength<L

(2)不允许碰撞:_wallDetectionFeelerLength=L

斥力

说了很多题外话,其实我们游戏开发中远没有那么精确,大部分是经验值,但往往经验值效果会很好。

我们继续来看控制力的计算,我们接下来遍历三条触须与墙面的交点,找出离物体最近的墙面与触须的交点,得出斥力:

Vec2 SteeringBehaviors::wallAvoidance(const std::vector<Wall>& walls)
{
    createFeelers();

    //temporary vars
    double dis2wall = 0.0;

    double dis2Closestwall = max_double;

    int closestWall = -1;

    Vec2 steeringForce = Vec2::ZERO;

    Vec2 point = Vec2::ZERO;

    Vec2 closestPoint = Vec2::ZERO;

    for (int i = 0; i < _feelers.size(); i++)
    {
        for (int j = 0; j < walls.size(); j++)
        {
            if (Wall::lineIntersection(_ownerVehicle->position(), _feelers.at(i), walls.at(j), dis2wall, point))
            {
                if (dis2wall < dis2Closestwall)
                {
                    dis2Closestwall = dis2wall;

                    closestWall = j;

                    closestPoint = point;
                }
            }
        }//next wall
        if (closestWall != -1)
        {
            Vec2 over = _feelers.at(i);

            steeringForce = walls.at(closestWall).normal()*over.getLength();
        }
    }//next feelers

    return steeringForce;
}

10、Hide躲猫猫

物体为了躲避危险,会藏到危险物看不见的地方,就像躲猫猫一样,此处的障碍体我们暂且设定为圆形,以后可以添加墙面等作为优化。

如图,我们找出躲藏点hiding:方向=障碍物圆心 - danger的position,位置=障碍物半径 + 定义的固定长度:

#define disFromBoundary 30.0
Vec2 SteeringBehaviors::getHidePosition(const BaseEntity* obstacle,const Vec2 target)
{
    double dis = obstacle->getBoundingRadius() + disFromBoundary;

    Vec2 target2Obstacle = (obstacle->position() - target).getNormalized();

    return (dis*target2Obstacle + obstacle->position());
}

如果障碍物是由墙面构成的几何体,我们重载该方法,将障碍物的圆心替换为几何体中心,其余基本相同,但是arrive方法要考虑到避免碰撞,需要写一个新的组合方法,实际遇到该问题时再来重写。

我们遍历距离物体一定范围内的所有障碍物,找出所有hiding,找出最近的点,靠近它:

Vec2 SteeringBehaviors::hide(const Vehicle* target, const std::vector<BaseEntity*> obstacles)
{
    double disSq2Closest = max_double;
    Vec2 closestHidingSpot = Vec2::ZERO;

    for_each(obstacles.cbegin(), obstacles.cend(), [this,target,&disSq2Closest,&closestHidingSpot](BaseEntity* eachObstacle)
    {
        Vec2 hidingSpot = getHidePosition(eachObstacle, target->position());

        double disSq = (hidingSpot - _ownerVehicle->position()).getLengthSq();

        if (disSq < disSq2Closest)
        {
            disSq2Closest = disSq;

            closestHidingSpot = hidingSpot;
        }
    }
    );
    //end for_each

    if (disSq2Closest == max_double)
    {
        return evade(target);
    }

    return arrive(closestHidingSpot, fast);
}

我们可以加上视觉元素让物体不那么“聪明”(这样会很笨拙),物体只有在看到danger时才会做出hide,只需要在hide方法最开头加一段:

    bool isHide = false;
    for_each(obstacles.cbegin(), obstacles.cend(), [this,target,&isHide](BaseEntity* obs){
        if (Seg::PointToSegDist(obs->position(), Seg(_ownerVehicle->position(), target->position())) < obs->getBoundingRadius())
            isHide = true;
    });
    if (isHide)
        return Vec2::ZERO;
    if (Vec2(_ownerVehicle->position(), target->position()).getLengthSq < _sightDis*_sightDis)
        return Vec2::ZERO;

11、PathFollowing路径跟随

玩过dota,war3,星际等游戏的童鞋应该知道shift操作就是按照你的指定路线操作,此处相同,我们规定path为多条首尾相接的直线的集合,物体会沿着path做循环或不循环的运动。

Path类

typedef std::function<void(Vec2,Vec2)> PointOperater;

class Path
{
private:
    std::list<Vec2> _pointsOfPath;
    std::list<Vec2>::iterator _currentPoint;
    bool _isLooped;
public:
#define two_PI 6.283185
    Path(bool isLooped):
        _isLooped(isLooped)
    {

    }

    Path(int numberOfPoints, Rect limit, bool isLooped):
        _isLooped(isLooped)
    {

    }

    virtual ~Path()
    {

    }

    Path(const Path& other)
    {
        assert(this != &other&&"same");
        this->set(other);
    }

    Path& operator=(const Path& other)
    {
        assert(this != &other&&"same");
        this->set(other);
        return *this;
    }

public:

    Vec2 currentPoint()const { return *_currentPoint; }

    bool isFinished()const { return _currentPoint == _pointsOfPath.end(); }

    void loopOn(){ this->_isLooped = true; }

    void loopOff(){ this->_isLooped = false; }

    bool isLooped()const { return _isLooped; }

    //methods for setting the path with either another Path or a list of vectors
    void set(std::list<Vec2> new_path){ _pointsOfPath = new_path; _currentPoint = _pointsOfPath.begin(); }
    void set(const Path& path){ _pointsOfPath = path.getPath(); _currentPoint = _pointsOfPath.begin(); }

    void clear(){ _pointsOfPath.clear(); }

    std::list<Vec2> getPath()const{ return _pointsOfPath; }
public:

    void addWayPoint(Vec2 new_point);

    void moveToNextPoint();

    void createRandomPath(int, Rect);

    void pathOperate(PointOperater);//对两个相邻点做回调操作,可以是纹理,动画等
};

void Path::moveToNextPoint()
{
    assert(this->_pointsOfPath.size() != 0 && "NULL");

    if (++_currentPoint == _pointsOfPath.end())
    {
        if (_isLooped)
        {
            _currentPoint = _pointsOfPath.begin();
        }
    }
}

void Path::addWayPoint(Vec2 point)
{
    this->_pointsOfPath.push_back(point);
}

void Path::createRandomPath(int num,Rect limit)
{
    double midX = (limit.getMaxX() + limit.getMinX()) / 2;
    double midY = (limit.getMaxY() + limit.getMinY()) / 2;

    double smallerRadius = std::min(midX, midY);
    double largerRadius = std::max(midX, midY);

    double dDegree = two_PI / num;

    for (int i = 0; i < num; i++)
    {
        Vec2 point = Vec2::ZERO;
        while (limit.containsPoint(point) && point != Vec2::ZERO)
        {
            double randomDegree = Random::Rand((i)*dDegree, (i + 1)*dDegree);

            double randomDis = Random::Rand(0, largerRadius);

            point = (randomDis, 0);
            point.rotateByAngle(Vec2(midX, midY), randomDegree);
        }
        _pointsOfPath.push_back(point);
    }
}

void Path::pathOperate(PointOperater func)
{
    std::list<Vec2>::const_iterator iter = _pointsOfPath.cbegin();

    Vec2 point = *iter++;

    while (iter != _pointsOfPath.end())
    {
        func(point,*iter);
        point = *iter++;
    }

    if (_isLooped)
    {
        func(*(--iter), *_pointsOfPath.cbegin());
    }
}

path类比较简单,只有一个createRandomPath比较麻烦,此处做了近似处理,只有测试的时候可能用一用,所以不用管它。

我为path的遍历添加了回调函数,我们可以其中执行cocos的render渲染等操作。

路径跟随就很简单了,找到最近的路径点,然后沿着点运动

Vec2 SteeringBehaviors::pathFollowing()
{
    //ignore evaluting if it‘ on path
    if (_currentPath->currentPoint().distance(_ownerVehicle->position()) < _pathPointSeekDis*_pathPointSeekDis);
    {
        _currentPath->moveToNextPoint();
    }

    if (!_currentPath->isFinished())
    {
        return seek(_currentPath->currentPoint());
    }
    else
    {
        return arrive(_currentPath->currentPoint(), normal);
    }
}  


准备写一个有关游戏底层算法,物理算法,以及AI(重点是机器学习在游戏中的应用)的长篇博客,欢迎大家指正交流╰( ̄▽ ̄)╯

时间: 2024-08-25 18:09:46

游戏底层逻辑,运动&&寻路(四)的相关文章

游戏底层逻辑,运动&amp;&amp;寻路(三)

上篇文章我们解释了几种基本的控制力,今天我们会讨论几种较为复杂的行为,涉及了碰撞,以及辅助图形进行运动控制. 7.Wander徘徊(巡逻) 徘徊(四处巡逻)是一种很常见的行为,但是要得到smoothly平滑的转向行为,并不是特别容易,这里有一种借用辅助圆实现的平滑移动. 如图,我们在物体前面构造了一个辅助圆,我们控制目标点,从而用seek方法控制运动行为,如果我们让目标点在圆周上运动,就可以产生一个力,他有如下属性: 一定向前(分力向物体的当前运动方向) 单帧转角范围为切线与x轴的夹角 大小为圆

cocos2d-x 游戏开发之有限状态机(FSM) (四)

cocos2d-x 游戏开发之有限状态机(FSM) (四) 虽然我们了解了FSM,并且可以写自己的FSM,但是有更好的工具帮我们完成这个繁琐的工作.SMC(http://smc.sourceforge.net/)就是这样的工具.下载地址: http://sourceforge.net/projects/smc/files/latest/download 在bin下面的Smc.jar是用于生成状态类的命令行工具.使用如下命令: $ java -jar Smc.jar Monkey.sm 1 真实世

Unity3D游戏开发从零单排(四) - 制作一个iOS游戏

提要 此篇是一个国外教程的翻译,虽然有点老,但是适合新手入门.自己去写代码,debug,布置场景,可以收获到很多.游戏邦上已经有前面两部分的译文,这里翻译的是游戏的最后一个部分. 欢迎回来 在第一篇中,我们学会了怎么在Unity中搭建游戏的场景,并且设置模型的物理属性. 在第二篇中,我们学会了怎么在unity中使用脚本,并且创建了大部分的游戏逻辑,包括投球和得分! 在这最后一节中,我们将会为用户创建一个菜单系统,并且和GameController进行交互,我们开始吧. 在设备上测试 到目前为止,

【文3】思维模型与底层逻辑摘录

1.思维模型就是对于信息的压缩,是帮助人们理解事物.解决问题的最佳框架.它就是我们大脑中用于做决策以及思考问题的工具箱,有时可能表现为一个用于分析的框架,再如当前手机中的APP.比如:SWOT 分析法 (S=strengths优势.W=weaknesses劣势.O=opportunities机会.T=threats威胁):有时可能表现为一个简短的理论,比如:心理账户理论.在设定工作目标时,我们可以打开一个名为"SMART"(S=Specific具体的.M=Measurable可衡量的.

游戏中逻辑线程和逻辑线程的并行

为什么要将游戏的渲染线程和逻辑线程分离? 游戏中渲染是一个非常耗时的操作,特别是相对复杂的游戏,渲染通常会占据一帧中的大部分时间.而高品质的游戏都会要求FPS在60,所以一帧的时间仅仅16毫秒. 如果要在16毫秒内完成逻辑和渲染双重的任务,对于大型游戏来说,通常是艰难的,即使在极度优化的情况下,也可能只能满足性能较好的设备,在性能较差的设备上,几乎不可能在16毫秒内完成所有任务. 所以如果将渲染线程和逻辑线程分离,那么理论上,他们各自都有16毫秒的时间来完成各自的任务,因为他们是并行进行的,这样

如何在Cocos2D游戏中实现A*寻路算法(一)

大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流之用,请勿进行商业用途.同时,转载时不要移除本申明.如产生任何纠纷,均与本博客所有人.发表该翻译稿之人无任何关系.谢谢合作! 该篇博客由iOS课程团队的Johann Fradj发布,他现在是一个全职开发iOS的开发者.他是Hot Apps Factory(其是App Cooker的创造者)的共同创建

股票市场运动的四个阶段 30日均线的13大操盘法

★股票市场运动的四个阶段: 以30日均线为标准,一次完整的股票市场循环运动必然包含上图框定的四个阶段,没有任何一只股票能够例外.30日均线是机构庄家操盘战略战术动作展开的生命线,其中的短线操作价值务必要引起我们绝对高度的重视.要把30均线对股票运动的极其重要性铭刻在我们的骨髓中.只要30日均线线的方向朝下,这只股票就绝对没有产生大行情的物质基础和市场条件,就绝对不是我们展开买进操作动作的目标对象.同时也说明该股票处于第四阶段D下跌阶段.此时正处于庄家战略性波段做空过程之中,行情下跌就是主旋律.间

致佳音: 推箱子游戏自动求解算法设计(四)

这一节是本文的核心内容,即推箱子游戏求解算法的设计思路过程 前面已经说过过,判断局面重复的最好标准不是局面完全一致,而是坐标排序相同且角色坐标通行 如下图,角色无论怎么移动,不推动箱子的时候,都能回到原来的位置,算作同一个局面: 再如下图,两个箱子互换位置,结果与没有移动箱子是一样的,所以排序箱子坐标以后一致,还是相同局面 问:有必要判断局面重复吗?是不是只是提升一下效率? 答:不是为了提升效率,而是为了能解出来,如果使用递归,重复的局面反复耗尽堆栈,而队列则耗尽内存 正如上图,反复推这两个箱子

libgdx游戏引擎开发笔记(四)文字显示BitmapFont

http://www.tuicool.com/articles/iAJbIj 由于Libgdx底层是用OpenGL实现的,所以Libgdx是可以支持中文的,在libgdx中的汉字都是通过贴图的方式显示,使用 BitmapFont和SpriteBatch 组合来完成文字的绘制,构造BitmapFont时需要一个描述文字构成的fnt文件,和一个提供文字图片的png文件.因此显示中文,归根结底就是读取一个包含中文信息的 .fnt文件 和相应的 .png文件 并展示出来的问题. 1.如何生成这两个文件那