BehaviourTree AI 行为树AI 实现的一些总结思考

Behaviour Tree的AI模块现在被很多地方所采用,前段时间自己实现了一个AI套件在Unity,中间也遇到了不少坑,现在来总结一下心得.

网上有很多行为树AI的介绍和实现,在这就不重复了.

更新频率过快导致的行为表现异常

行为树模块需要周期性的更新,先说说连续几个周期存在的问题。如果这个周期满足条件是Actor A走到Actor B周围的随机位置的Action,于是Actor A在执行这个行为的时候先找到了一个Actor B的随机位置,然后走过去。下一个周期如果依然满足条件是执行这个Action,于是Actor A又立即重新执找了一个随机位置走过去。下下一个周期再次随机了…

如此反复就表现为Actor A在Actor B附近转来转去,如果更新频率过快,很有可能还会各种抖动的移动,非常突兀的效果.

不仅仅是走路这个行为,其他行为很有可能也遇到类似的情况.

有人会说,放慢AI的更新频率不就好了吗?

但是你很难把握这个AI更新频率到底是多少,如果过慢,就会出现Actor呆滞在原地显得呆板生硬,过快的话,就出现了上述描述的情况。经验上来说不能通过放慢AI更新频率解决这个问题.

我的解决方法是,会有一个额外的AI调度器,来决定这个AI行为实在会真的被执行。比如说对于走到Actor B随机位置的这个Action,如果上一次还是执行这个相同的Action,那么会检测一下上一次的Action是否执行完成了,如果执行完成了,那么才会真正的执行.

这样尽管行为树不断决策出相同的Action,但是却不会造成Actor被更新过快的问题,并且也不会出现呆滞在原地的情况.

Action执行间隔问题

另外还有一个Action执行间隔的问题,比如说这段时间都满足攻击的Action执行,于是Actor A开始不断攻击Actor B,但是攻击频率如此之高可能是超过了理想的预期,比如说让游戏变得更难,这可能是设计者不想要的情况.

我在Action中添加了一个执行间隔的设计,让这个Action执行的时候每次都有间隔时间,下一次如果尽管还会执行到这个Action,但是如果还没超过每次执行的间隔时间,那么这个Action就不会被执行.

那么如果这个Action不执行的话,Actor又有可能会呆滞在原地,因为Ta需要等待间隔时间到达。为了避免这一种情况,我在单个Action中添加了一个间隔使用的Action,当处于间隔时期时,就执行这个间隔使用的Action,比如很有可能是在原地随便走动一下。这样就避免了在攻击的间隙,Actor呆滞的问题.

Continue reading BehaviourTree AI 行为树AI 实现的一些总结思考

Unity3D的一半黑白一半彩色材质Shader

特效人员可能需要一个模型上半部分显示黑白,下半部分显示正常的纹理颜色这样的效果。并且可以通过参数可调节黑白范围。

脚本

拿到每个顶点的世界坐标系的值,比对Y值,大于就黑白,小于就正常。关键在于找到顶点的最高点和最低点。 于是有了这个脚本绑定在对应的Mesh Object上。用来告知shader这个Mesh的最高点和最低点。

using UnityEngine;

public class MeshVerticesInfo : MonoBehaviour
{
    private Vector4 mBeginVertex = Vector4.zero;
    private Vector4 mEndVertex = Vector4.zero;
    private float mMeshHeight = 0.0f;

    void Start()
    {
        MeshFilter meshFilter = GetComponent<MeshFilter>();

        if (meshFilter == null)
        {
            return;
        }

        Mesh mesh = meshFilter.mesh;

        if (mesh == null)
        {
            return;
        }

        Vector3[] vertices = mesh.vertices;

        mBeginVertex.y = float.MaxValue;
        mEndVertex.y = float.MinValue;

        // 拿到最低点和最高点
        int i = 0;
        foreach (Vector3 vertex in vertices)
        {
            if (i <= 0)
            {
                mBeginVertex = vertex;
                mEndVertex = vertex;

                mBeginVertex = transform.TransformPoint(vertex);
                mEndVertex = transform.TransformPoint(vertex);
            }

            Vector3 vertexWorldPos = transform.TransformPoint(vertex);

            if (vertexWorldPos.y <= mBeginVertex.y)
            {
                mBeginVertex = vertexWorldPos;
            }

            if (vertexWorldPos.y > mEndVertex.y)
            {
                mEndVertex = vertexWorldPos;
            }

            ++i;
        }

        mMeshHeight = mEndVertex.y - mBeginVertex.y;
    }
    void Update()
    {
        renderer.material.SetVector("_BeginVertex", mBeginVertex);
        renderer.material.SetVector("_EndVertex", mEndVertex);
        renderer.material.SetFloat("_MeshHeight", mMeshHeight);
    }
}

Shader

Continue reading Unity3D的一半黑白一半彩色材质Shader

Unity3D的高光shader(BumpedSpecular)改进:支持高光贴图

Unity3D引擎确实将shader的编写方便了不少(当然我也用过Unreal Engine 4那极其强大的结合Blueprint材质编辑器),Surface Shader的出现也方便了很多.

Unity自带的BumpedSpecular shader可以得到一些反射高光的效果,但是却不能支持高光贴图,改进了一下,代码如下:

Shader "Custom/BumpedHighlightSpecular" 
{
    Properties 
    {
        _Color ("Main Color", Color) = (1,1,1,1)
        _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
        _Shininess ("Shininess", Range (0.03, 1)) = 0.078125
        _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
        _BumpMap ("Normalmap", 2D) = "bump" {}
        _SpecularTextureGloss("Specular(RGB)Gloss(A)", 2D) = "black" {}
        _SpecularMultiple("Specular Multiple", Float) = 1
    }
    SubShader 
    { 
        Tags 
        { 
            "RenderType"="Opaque"  
        }
        LOD 400

        CGPROGRAM
        #pragma surface surf BlinnPhong

        sampler2D _MainTex;
        sampler2D _BumpMap;
        fixed4 _Color;
        half _Shininess;
        sampler2D _SpecularTextureGloss;
        half _SpecularMultiple;
        fixed4 _SpecularColor;

        struct Input 
        {
            half2 uv_MainTex;
            half2 uv_BumpMap;
            half2 uv_SpecularTextureGloss;
        };

        void surf (Input IN, inout SurfaceOutput o) 
        {
            fixed4 mainTexture = tex2D(_MainTex, IN.uv_MainTex);
            fixed4 specularTexture = tex2D(_SpecularTextureGloss,(IN.uv_SpecularTextureGloss.xyxy).xy);
            fixed4 specular = _Shininess.xxxx + specularTexture.aaaa;
            fixed4 specular2 = specularTexture + _SpecColor;
            fixed4 gloss = specular2 * _SpecularMultiple.xxxx;

            _SpecColor = _SpecColor * specularTexture;

            o.Albedo = mainTexture.rgb * _Color.rgb;
            o.Gloss = gloss;
            o.Alpha = mainTexture.a * _Color.a;
            o.Specular = specular ;
            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
        }

        ENDCG
    }

    FallBack "Specular"
}

用的光照模型依然是BlinnPhong,多了一张高光贴图,另外高光贴图的颜色也会影响到最终的颜色.

多了个_SpecularMultiple参数来叠加高光效果.

一个C#的泛型工厂

遇到类似以下情况:

class Base
{
    public enum TestType
    {
        TYPE_A,
        TYPE_B
    }
}

class TypeA:Base
{

}

class TypeB:Base
{

}

根据枚举类型不同而创建不同子类的时候,我们需要一个工厂来进行管理。
在泛型工厂中,需要保存构造函数指针和对应的枚举类型映射,于是需要有添加创建器的操作。
一个很方便使用的泛型工厂代码如下:

Continue reading 一个C#的泛型工厂

函数封装的改进(C++ 11不定长模板的加入)

在上文中,介绍了一种使用C++ 11动态类型推导封装函数的方法,但是依然没有摆脱函数不定参数封装的传统模式。今天借助C++ 11的不定长模板参数功能,来解决掉这个问题.

首先回顾一下要达到的要求:

  • 可以支持封装静态函数(全局函数)和类成员函数
  • 被封装的函数可以接受任意数量和类型的参数
  • 调用者可以拿到直接的真正的被封装函数的返回值和返回类型
  • 封装后的成员函数,可以在真正运行的时候才接受绑定的this object

这篇文章要解决的就是第二点.
封装依然得分为静态(全局)函数和类成员函数分别封装.

静态(全局)函数

代码:

template<class Func, class ... Args>
class Callable
{
private:
    Func functor_;

public:
    Callable(Func functor)
        : functor_(functor)
    {

    }

    auto run(Args&&... args)    ->decltype(functor_(args...))
    {
        return functor_(args...);
    }
};

template <class Ret, class ...Args>
auto MakeCallable(Ret(*func)(Args...)) ->decltype(new Callable<Ret(*)(Args...), Args...>(func))
{
    return new Callable<Ret(*)(Args...),Args...>(func);
}

和前文不同的地方在于,多了一个…Args模板参数,这个表示不定长模板参数,使用…来标记.这样就能做到匹配到任意参数个数的函数,不再需要像前文那样根据参数个数不同写多个这样的包装函数实现.

Continue reading 函数封装的改进(C++ 11不定长模板的加入)

一种函数Callback的封装(使用到C++11)

本文想要实现的函数封装功能有如下:

  1. 可以支持封装静态函数(全局函数)和类成员函数
  2. 被封装的函数可以接受任意数量和类型的参数
  3. 调用者可以拿到直接的真正的被封装函数的返回值和返回类型
  4. 封装后的成员函数,可以在真正运行的时候才接受绑定的this object

原理

无论是__cdecl(缺省)还是__thiscall函数,我们都可以通过函数指针存储起来,借助C++的模板,于是我们可以存储任意类型的函数指针。同时借助C++11的返回值类型推导,于是我们可以让使用者拿到真正的函数返回值. 如果你不熟悉C++11的返回值类型推导内容,请点击这里.

实现

静态函数(全局函数)

先直接看代码:

template <class Func> 
class Callback0 
{
public:
    typedef decltype(Func()) result_type;

    Callback0(Func functor)
        : functor_(functor)
    { }

    auto run() ->decltype(Func())
    { 
        return functor_();
    }
private:
    Func functor_;
};

template <class Func,class A0>
class Callback1
{
public:
    typedef decltype(Func()(a0)) result_type;

    Callback1(Func functor)
        : functor_(functor)
    {

    }

    auto run(A0 a0) ->decltype(Func()(a0))
    {
        return functor_(a0);
    }

private: 

    Func functor_;
};

template <class Func, class A0, class A1>
class Callback2
{
public:
    typedef decltype(Func()(a0,a1)) result_type;

    Callback2(Func functor)
        : functor_(functor)
    {

    }

    auto run(A0 a0, A1 a1) ->decltype(Func()(a0, a1))
    {
        return functor_(a0,a1);
    }

private:

    Func functor_;
};

template <class Ret>
auto MakeCallback(Ret(*func)()) ->decltype(new Callback0<Ret(*)()>(func))
{
    return new Callback0<Ret(*)()>(func);
}

template <class Ret, class A0>
auto MakeCallback(Ret(*func)(A0)) ->decltype(new Callback1<Ret(*)(A0), A0>(func))
{
    return new Callback1<Ret(*)(A0), A0>(func);
}

template <class Ret, class A0, class A1>
auto MakeCallback(Ret(*func)(A0, A1))   ->decltype(new Callback2<Ret(*)(A0,A1), A0,A1>(func))
{
    return new Callback2<Ret(*)(A0,A1), A0, A1>(func);
}

这里实现了一个最多支援两个参数的静态函数封装,但是支援无限多个参数,也只需要再写多个类似的结构即可。这个部分其实是和早前的C++规范是类似的。

Continue reading 一种函数Callback的封装(使用到C++11)