从上古卷轴中形形色色的人物,到NBA2K中挥洒汗水的球员,从使命召唤中诡计多端的敌人,到刺客信条中栩栩如生的人群。游戏AI几乎存在于游戏中的每个角落,默默构建出一个令人神往的庞大游戏世界。

那么这些复杂的AI又是怎么实现的呢?下面就让我们来了解并亲手实现一下游戏AI基础架构之一的行为树。

行为树简介

行为树是一种树状的数据结构,树上的每一个节点都是一个行为。每次调用会从根节点开始遍历,通过检查行为的执行状态来执行不同的节点。他的优点是耦合度低扩展性强,每个行为可以与其他行为完全独立。目前的行为树已经可以将几乎任意架构(如规划器,效用论等)应用于AI之上。

  1. class BehaviorTree
  2. {
  3. public:
  4. BehaviorTree(Behavior* InRoot) { Root = InRoot; }
  5. void Tick()
  6. {
  7. Root->Tick();
  8. }
  9. bool HaveRoot() { return Root?true:false; }
  10. void SetRoot(Behavior* InNode) { Root= InNode; }
  11. void Release() { Root->Release(); }
  12. private:
  13. Behavior* Root;
  14. };

复制代码

上面提供了行为树的实现,行为树有一个根节点和一个Tick()方法,在游戏过程中每个一段时间会调用依次Tick方法,令行为树从根节点开始执行。

行为(behavior)

行为(behavior)是行为树最基础的概念,是几乎所有行为树节点的基类,是一个抽象接口,而如动作条件等节点则是它的具体实现。
下面是Behavior的实现,省略掉了一些简单的判断状态的方法完整源码可以参照文尾的github链接

  1. class Behavior
  2. {
  3. public:
  4. //释放对象所占资源
  5. virtual void Release() = 0;
  6. //包装函数,防止打破调用契约
  7. EStatus Tick();
  8. EStatus GetStatus() { return Status; }
  9. virtual void AddChild(Behavior* Child){};
  10. protected:
  11. //创建对象请调用Create()释放对象请调用Release()
  12. Behavior():Status(EStatus::Invalid){}
  13. virtual ~Behavior() {}
  14. virtual void OnInitialize() {};
  15. virtual EStatus Update() = 0;
  16. virtual void OnTerminate(EStatus Status) {};
  17. protected:
  18. EStatus Status;
  19. };

复制代码

Behavior接口是所有行为树节点的核心,且我规定所有节点的构造和析构方法都必须是protected,以防止在栈上创建对象,所有的节点对象通过Create()静态方法在堆上创建,通过Release()方法销毁,由于Behavior是个抽象接口,故没有提供Create()方法,本接口满足如下契约

在Update方法被首次调用前,调用一次OnInitialize函数,负责初始化等操作

Update()方法在行为树每次更新时调用且仅调用一次。

当行为不再处于运行状态时,调用一次OnTerminate(),并根据返回状态不同执行不同的逻辑

为了保证契约不被打破,我们将这三个方法包装在Tick()方法里。Tick()的实现如下

  1. //update方法被首次调用前执行OnInitlize方法,每次行为树更新时调用一次update方法
  2. //当刚刚更新的行为不再运行时调用OnTerminate方法
  3. if (Status != EStatus::Running)
  4. {
  5. OnInitialize();
  6. }
  7. Status = Update();
  8. if (Status != EStatus::Running)
  9. {
  10. OnTerminate(Status);
  11. }

复制代码

其中返回值Estatus是一个枚举值,表示节点运行状态。

  1. enum class EStatus:uint8_t
  2. {
  3. Invalid,   //初始状态
  4. Success,   //成功
  5. Failure,   //失败
  6. Running,   //运行
  7. Aborted,   //终止
  8. };

复制代码

动作(Action)

动作是行为树的叶子节点,表示角色做的具体操作(如攻击,上弹,防御等),负责改变游戏世界的状态。动作节点可直接继承自Behavior节点,通过实现不同的Update()方法实现不同的逻辑,在OnInitialize()方法中获取数据和资源,在OnTerminate中释放资源。

  1. //动作基类
  2. class Action :public Behavior
  3. {
  4. public:
  5. virtual void Release() { delete this; }
  6. protected:
  7. Action() {}
  8. virtual ~Action() {}
  9. };

复制代码

在这里我实现了一个动作基类,主要是为了一个公用的Release方法负责释放节点内存空间,所有动作节点均可继承自这个方法

条件

条件同样是行为树的叶子节点,用于查看游戏世界信息(如敌人是否在攻击范围内,周围是否有可攀爬物体等),通过返回状态表示条件的成功。

  1. //条件基类
  2. class Condition :public Behavior
  3. {
  4. public:
  5. virtual void Release() { delete this; }
  6. protected:
  7. Condition(bool InIsNegation):IsNegation(InIsNegation) {}
  8. virtual ~Condition() {}
  9. protected:
  10. //是否取反
  11. bool  IsNegation=false;
  12. };

复制代码

这里我实现了条件基类,一个IsNegation来标识条件是否取反(比如是否看见敌人可以变为是否没有看见敌人)

装饰器(Decorator)

装饰器(Decorator)是只有一个子节点的行为,顾名思义,装饰即是在子节点的原有逻辑上增添细节(如重复执行子节点,改变子节点返回状态等)

  1. <p>//装饰器</p><p>class Decorator :public Behavior</p><p>{</p><p>public:</p><p>    virtual void AddChild(Behavior* InChild) { Child=InChild; }</p><p>protected:</p><p>    Decorator() {}</p><p>    virtual ~Decorator(){}</p><p>    Behavior* Child;</p><p>};</p>

复制代码

实现了装饰器基类,下面我们来实现下具体的装饰器,也就是上面提到的重复执行多次子节点的装饰器

  1. <p>class Repeat :public Decorator</p><p>{</p><p>public:</p><p>    static Behavior* Create(int InLimited) { return new Repeat(InLimited); }</p><p>    virtual void Release() { Child->Release(); delete this; }</p><p>protected:</p><p>    Repeat(int InLimited) :Limited(InLimited) {}</p><p>    virtual ~Repeat(){}</p><p>    virtual void OnInitialize() { Count = 0; }</p><p>    virtual EStatus Update()override;</p><p>    virtual Behavior* Create() { return nullptr; }</p><p>protected:</p><p>    int Limited = 3;</p><p>    int Count = 0;</p><p>};</p>

复制代码

正如上面提到的,Create函数负责创建节点,Release负责释放

其中Update()方法的实现如下

  1. <p>EStatus Repeat::Update()</p><p>{</p><p>    while (true)</p><p>    {</p><p>        Child->Tick();</p><p>        if (Child->IsRunning())return EStatus::Success;</p><p>        if (Child->IsFailuer())return EStatus::Failure;</p><p>        if (++Count == Limited)return EStatus::Success;</p><p>        Child->Reset();</p><p>    }</p><p>    return EStatus::Invalid;</p><p>}</p>

复制代码

逻辑很简单,如果执行失败就立即返回,二手手机拍卖执行中就继续执行,执行成功就把计数器+1重复执行

复合行为

我们将行为树中具有多个子节点的行为称为复合节点,通过复合节点我们可以将简单节点组合为更有趣更复杂的行为逻辑。

下面实现了一个符合节点的基类,将一些公用的方法放在了里面(如添加清除子节点等)

  1. <p>//复合节点基类</p><p>class Composite:public Behavior</p><p>{  </p><p>    virtual void AddChild(Behavior* InChild) override{Childern.push_back(InChild);}</p><p>    void RemoveChild(Behavior* InChild);</p><p>    void ClearChild() { Childern.clear(); }</p><p>    virtual void Release()</p><p>    {</p><p>        for (auto it : Childern)</p><p>        {</p><p>            it->Release();</p><p>        }</p><p>
  2. </p><p>        delete this;</p><p>    }</p><p>
  3. </p><p>protected:</p><p>    Composite() {}</p><p>    virtual ~Composite() {}</p><p>    using Behaviors = std::vector<Behavior*>;</p><p>    Behaviors Childern;</p><p>};</p>

复制代码

顺序器(Sequence)

顺序器(Sequence)是复合节点的一种,它依次执行每个子行为,直到所有子行为执行成功或者有一个失败为止。

  1. <p>//顺序器:依次执行所有节点直到其中一个失败或者全部成功位置</p><p>class Sequence :public Composite</p><p>{</p><p>public:</p><p>    virtual std::string Name() override { return "Sequence"; }</p><p>    static Behavior* Create() { return new Sequence(); }</p><p>protected:</p><p>    Sequence() {}</p><p>    virtual ~Sequence(){}</p><p>    virtual void OnInitialize() override { CurrChild = Childern.begin();}</p><p>    virtual EStatus Update() override;</p><p>
  2. </p><p>protected:</p><p>    Behaviors::iterator CurrChild;</p><p>};</p>

复制代码

其中Update()方法的实现如下

  1. <p>EStatus Sequence::Update()</p><p>{</p><p>    while (true)</p><p>    {</p><p>        EStatus s = (*CurrChild)->Tick();</p><p>        //如果执行成功了就继续执行,否则返回</p><p>        if (s != EStatus::Success)</p><p>            return s;</p><p>        if (++CurrChild == Childern.end())</p><p>            return EStatus::Success;</p><p>    }</p><p>    return EStatus::Invalid;  //循环意外终止</p><p>}</p>

复制代码

选择器(Selector)

选择器(Selector)是另一种常用的复合行为,它会依次执行每个子行为直到其中一个成功执行或者全部失败为止

由于与顺序器仅仅是Update函数不同,下面仅贴出Update方法

  1. <p>EStatus Selector::Update()</p><p>{</p><p>    while (true)</p><p>    {</p><p>        EStatus s = (*CurrChild)->Tick();</p><p>        if (s != EStatus::Failure)</p><p>            return s;   </p><p>        //如果执行失败了就继续执行,否则返回</p><p>        if (++CurrChild == Childern.end())</p><p>            return EStatus::Failure;</p><p>    }</p><p>    return EStatus::Invalid;  //循环意外终止</p><p>}</p>

复制代码

并行器(Parallel)

顾名思义,并行器(Parallel)是一种让多个行为并行执行的节点。但仔细观察便会发现实际上只是他们的更新函数在同一帧被多次调用而已。

  1. <p>//并行器:多个行为并行执行</p><p>class Parallel :public Composite</p><p>{</p><p>public:</p><p>    static Behavior* Create(EPolicy InSucess, EPolicy InFailure){return new Parallel(InSucess, InFailure); }</p><p>    virtual std::string Name() override { return "Parallel"; }</p><p>
  2. </p><p>protected:</p><p>    Parallel(EPolicy InSucess, EPolicy InFailure) :SucessPolicy(InSucess), FailurePolicy(InFailure) {}</p><p>    virtual ~Parallel() {}</p><p>    virtual EStatus Update() override;</p><p>    virtual void OnTerminate(EStatus InStatus) override;</p><p>
  3. </p><p>protected:</p><p>    EPolicy SucessPolicy;</p><p>    EPolicy FailurePolicy;</p><p>};</p>

复制代码

这里的Epolicy是一个枚举类型,表示成功和失败的条件(是成功或失败一个还是全部成功或失败)

  1. <p>//Parallel节点成功与失败的要求,是全部成功/失败,还是一个成功/失败</p><p>enum class EPolicy :uint8_t</p><p>{</p><p>    RequireOne,</p><p>    RequireAll,</p><p>};</p>

复制代码

update函数实现如下

  1. <p>EStatus Parallel::Update()</p><p>{</p><p>    int SuccessCount = 0, FailureCount = 0;</p><p>    int ChildernSize = Childern.size();</p><p>    for (auto it : Childern)</p><p>    {</p><p>        if (!it->IsTerminate())</p><p>            it->Tick();</p><p>
  2. </p><p>        if (it->IsSuccess())</p><p>        {</p><p>            ++SuccessCount;</p><p>            if (SucessPolicy == EPolicy::RequireOne)</p><p>            {</p><p>                it->Reset();</p><p>                return EStatus::Success;</p><p>            }</p><p>                </p><p>        }</p><p>
  3. </p><p>        if (it->IsFailuer())</p><p>        {</p><p>            ++FailureCount;</p><p>            if (FailurePolicy == EPolicy::RequireOne)</p><p>            {</p><p>                it->Reset();</p><p>                return EStatus::Failure;</p><p>            }       </p><p>        }</p><p>    }</p><p>
  4. </p><p>    if (FailurePolicy == EPolicy::RequireAll&&FailureCount == ChildernSize)</p><p>    {</p><p>        for (auto it : Childern)</p><p>        {</p><p>            it->Reset();</p><p>        }</p><p>        </p><p>        return EStatus::Failure;</p><p>    }</p><p>    if (SucessPolicy == EPolicy::RequireAll&&SuccessCount == ChildernSize)</p><p>    {</p><p>        for (auto it : Childern)</p><p>        {</p><p>            it->Reset();</p><p>        }</p><p>        return EStatus::Success;</p><p>    }</p><p>
  5. </p><p>    return EStatus::Running;</p><p>}</p>

复制代码

在代码中,并行器每次更新都执行每一个尚未终结的子行为,并检查成功和失败条件,如果满足则立即返回。

另外,当并行器满足条件提前退出时,所有正在执行的子行为也应该立即被终止,我们在OnTerminate()函数中调用每个子节点的终止方法

  1. <p>void Parallel::OnTerminate(EStatus InStatus)</p><p>{</p><p>     for (auto it : Childern)</p><p>    {</p><p>        if (it->IsRunning())</p><p>            it->Abort();</p><p>    }</p><p>}</p>

复制代码

监视器(Monitor)

监视器是并行器的应用之一,通过在行为运行过程中不断检查是否满足某条件,如果不满足则立刻退出。将条件放在并行器的尾部即可。

主动选择器

主动选择器是选择器的一种,与普通的选择器不同的是,主动选择器会不断的主动检查已经做出的决策,并不断的尝试高优先级行为的可行性,当高优先级行为可行时胡立即打断低优先级行为的执行(如正在巡逻的过程中发现敌人,即时中断巡逻,立即攻击敌人)。

其Update()方法和OnInitialize方法实现如下

  1. <p>//初始化时将CurrChild初始化为子节点的末尾</p><p>virtual void OnInitialize() override { CurrChild = Childern.end(); }</p><p>
  2. </p><p>    EStatus ActiveSelector::Update()</p><p>    {</p><p>        //每次执行前先保存的当前节点</p><p>        Behaviors::iterator Previous = CurrChild;</p><p>        //调用父类OnInlitiallize函数让选择器每次重新选取节点</p><p>        Selector::OnInitialize();</p><p>        EStatus result = Selector::Update();</p><p>        //如果优先级更高的节点成功执行或者原节点执行失败则终止当前节点的执行</p><p>        if (Previous != Childern.end()&CurrChild != Previous)</p><p>        {</p><p>            (*Previous)->Abort();   </p><p>        }</p><p>
  3. </p><p>        return result;</p><p>    }</p>

复制代码

示例

这里我创建了一名角色,该角色一开始处于巡逻状态,一旦发现敌人,先检查自己生命值是否过低,如果是就逃跑,否则就攻击敌人,攻击过程中如果生命值过低也会中断攻击,立即逃跑,如果敌人死亡则立即停止攻击,这里我们使用了构建器来创建了一棵行为树,关于构建器的实现后面会讲到,这里每个函数创建了对应函数名字的节点,

  1. <p>//构建行为树:角色一开始处于巡逻状态,一旦发现敌人,先检查自己生命值是否过低,如果是就逃跑,否则就攻击敌人,攻击过程中如果生命值过低也会中断攻击,立即逃跑,如果敌人死亡则立即停止攻击</p><p>    BehaviorTreeBuilder* Builder = new BehaviorTreeBuilder();</p><p>    BehaviorTree* Bt=Builder</p><p>        ->ActiveSelector()</p><p>            ->Sequence()</p><p>                ->Condition(EConditionMode::IsSeeEnemy,false)</p><p>                     ->Back()       </p><p>                ->ActiveSelector()</p><p>                     -> Sequence()</p><p>                          ->Condition(EConditionMode::IsHealthLow,false)</p><p>                               ->Back()</p><p>                          ->Action(EActionMode::Runaway)</p><p>                                ->Back()</p><p>                          ->Back()</p><p>                    ->Monitor(EPolicy::RequireAll,EPolicy::RequireOne)</p><p>                          ->Condition(EConditionMode::IsEnemyDead,true)</p><p>                                ->Back()</p><p>                          ->Action(EActionMode::Attack)</p><p>                                ->Back()</p><p>                          ->Back()</p><p>                    ->Back()</p><p>                ->Back()</p><p>            ->Action(EActionMode::Patrol)</p><p>    ->End();</p><p>
  2. </p><p>    delete Builder;</p>

复制代码

然后我通过一个循环模拟行为树的执行。同时在各条件节点内部通过随机数表示条件是否执行成功(具体见文末github源码)

  1. <p>    //模拟执行行为树</p><p>    for (int i = 0; i < 10; ++i)</p><p>    {</p><p>        Bt->Tick();</p><p>        std::cout << std::endl;</p><p>    }</p>

复制代码

执行结果如下,由于随机数的存在每次执行结果都不一样

构建器的实现

上面创建行为树的时候用到了构建器,下面我就介绍一下自己的构建器实现

  1. <p>//行为树构建器,用来构建一棵行为树,通过前序遍历方式配合Back()和End()方法进行构建</p><p>class BehaviorTreeBuilder</p><p>{</p><p>public:</p><p>    BehaviorTreeBuilder() { }</p><p>    ~BehaviorTreeBuilder() { }</p><p>    BehaviorTreeBuilder* Sequence();</p><p>    BehaviorTreeBuilder* Action(EActionMode ActionModes);</p><p>    BehaviorTreeBuilder* Condition(EConditionMode ConditionMode,bool IsNegation);</p><p>    BehaviorTreeBuilder* Selector();</p><p>    BehaviorTreeBuilder* Repeat(int RepeatNum);</p><p>    BehaviorTreeBuilder* ActiveSelector();</p><p>    BehaviorTreeBuilder* Filter();</p><p>    BehaviorTreeBuilder* Parallel(EPolicy InSucess, EPolicy InFailure);</p><p>    BehaviorTreeBuilder* Monitor(EPolicy InSucess, EPolicy InFailure);</p><p>    BehaviorTreeBuilder* Back();</p><p>    BehaviorTree* End();</p><p>
  2. </p><p>private:</p><p>    void AddBehavior(Behavior* NewBehavior);</p><p>
  3. </p><p>private:</p><p>    Behavior* TreeRoot=nullptr;</p><p>    //用于存储节点的堆栈</p><p>    std::stack<Behavior*> NodeStack;</p><p>};</p><p>BehaviorTreeBuilder* BehaviorTreeBuilder::Sequence()</p><p>{</p><p>    Behavior* Sq=Sequence::Create();</p><p>    AddBehavior(Sq);</p><p>    return this;</p><p>}</p><p>
  4. </p><p>void BehaviorTreeBuilder::AddBehavior(Behavior* NewBehavior)</p><p>{</p><p>    assert(NewBehavior);</p><p>    //如果没有根节点设置新节点为根节点</p><p>    if (!TreeRoot)</p><p>    {</p><p>        TreeRoot=NewBehavior;</p><p>    }</p><p>    //否则设置新节点为堆栈顶部节点的子节点</p><p>    else</p><p>    {</p><p>        NodeStack.top()->AddChild(NewBehavior);</p><p>    }</p><p>
  5. </p><p>    //将新节点压入堆栈</p><p>    NodeStack.push(NewBehavior);</p><p>}</p><p>
  6. </p><p>BehaviorTreeBuilder* BehaviorTreeBuilder::Back()</p><p>{</p><p>    NodeStack.pop();</p><p>    return this;</p><p>}</p><p>
  7. </p><p>BehaviorTree* BehaviorTreeBuilder::End()</p><p>{</p><p>    while (!NodeStack.empty())</p><p>    {</p><p>        NodeStack.pop();</p><p>    }</p><p>    BehaviorTree* Tmp= new BehaviorTree(TreeRoot);</p><p>    TreeRoot = nullptr;</p><p>    return Tmp;</p><p>}</p>

复制代码

在上面的实现中,我在每个方法里创建对应节点,检测当前是否有根节点,如果没有则将其设为根节点,如果有则将其设为堆栈顶部节点的子节点,随后将其压入堆栈,每次调用back则退栈,每个创建节点的方法都返回this以方便调用下一个方法,最后通过End()表示行为树创建完成并返回构建好的行为树。

那么上面就是行为树的介绍和实现了,下一篇我们将对行为树进行优化,慢慢进入第二代行为树。

游戏AI——行为树理论及实现相关推荐

  1. 游戏ai 行为树_游戏AI –行为树简介

    游戏ai 行为树 游戏AI是一个非常广泛的主题,尽管有很多资料,但我找不到能以较慢且更易理解的速度缓慢介绍这些概念的东西. 本文将尝试解释如何基于行为树的概念来设计一个非常简单但可扩展的AI系统. 什 ...

  2. 游戏AI - 行为树Part2:框架

    上次提到,行为树可以让代码更加模块化,也可以提高重用性.这次我们就来看看一个行为树框架是什么样的. 如果你对行为树比较陌生,可以先浏览一下游戏AI - 行为树Part1:简介. 关键词 在展开之前,我 ...

  3. 游戏AI –行为树简介

    游戏AI是一个非常广泛的主题,尽管有很多资料,但我找不到能以较慢,更容易理解的速度缓慢介绍这些概念的东西. 本文将尝试解释如何基于行为树的概念来设计一个非常简单但可扩展的AI系统. 什么是AI? 人工 ...

  4. 谈一谈游戏AI - 行为树

    不要用过去的成绩看未来,而是要用未来的眼睛看现在. 郑重说明:本文适合对游戏开发感兴趣的小白初学者,本人力图将事物用简单的语言表达清楚,但水平有限,能力一般,文章如有错漏之处,还望批评指正. 在本系列 ...

  5. 人工智能_游戏AI –行为树简介

    人工智能 游戏AI是一个非常广泛的主题,尽管有很多材料,但我找不到能以较慢,更易理解的速度缓慢引入这些概念的东西. 本文将尝试解释如何基于行为树的概念来设计一个非常简单但可扩展的AI系统. 什么是AI ...

  6. 游戏AI—行为树研究及实现(转自月夜魔术师 https://segmentfault.com/a/1190000012397660)

    行为树简介 行为树是一种树状的数据结构,树上的每一个节点都是一个行为.每次调用会从根节点开始遍历,通过检查行为的执行状态来执行不同的节点.他的优点是耦合度低扩展性强,每个行为可以与其他行为完全独立.目 ...

  7. 如何建立一个完整的游戏AI

    http://blog.friskit.me/2012/04/how-to-build-a-perfect-game-ai/ 人工智能(Artificial Intelligence)在游戏中使用已经 ...

  8. AI行为树的基础运作原理

    欢迎捉虫! 之前我研究了一下基于switch case语句的FSM状态机的使用,后来遇到了很多问题. 比如当角色的行为很多时,代码结构相当混乱(你需要考虑每一种状态之间的联系). 所以,当角色的行为愈 ...

  9. 游戏AI:只是AI间的游戏,还是游戏的未来?

    前言背景 1. 雅达利的崛起与沉沦 1974年,一个名叫史蒂夫·乔布斯的年轻人来到了雅达利(Atari)公司位于洛思加图斯的总部,拿着一块他朋友沃兹尼亚克做的电路板,手舞足蹈的比划,试图让对方相信这个 ...

最新文章

  1. Android 光线传感器的调用
  2. [转]sql,N/$/#/@的含义和作用
  3. mysql新建用户只能查看试图_Sql Server:创建用户并指定该用户只能看指定的视图,除此之外的都不让查看。...
  4. 单点登录原理与简单实现【转载】
  5. oracle das系统,分布式声波传感系统DAS
  6. 【运动学】基于matlab GUI模拟小球自由落体【含Matlab源码 1630期】
  7. 关于C语言中fseek函数的使用
  8. sqlmap运行mysql命令_sqlmap命令总结
  9. 深度学习是什么,深度学习概念的基本理解?
  10. 利用NEO与Unity制作游戏(第2部分)
  11. SD卡变成RAW格式怎么办?SD卡RAW格式的解决办法
  12. 如何清空c盘只剩系统_使用win10不久,C盘只剩下500MB?这样操作,我清理出了30G空间!...
  13. 基于INA199的电机电流检测尝试
  14. android中edittext属性
  15. 第一次接触APK【破解纪实】
  16. NJFU软件需求分析试卷
  17. HowTo如何制作一个文字冒险游戏-里篇(1)
  18. vue项目配置public静态资源路径访问
  19. 基础班-系统班英语词汇补充讲义后100题(下半部分)
  20. 谷歌建议提示,移除多余的关键字,为什么?

热门文章

  1. mysql主从复制缺陷_mysql主从复制及遇到的坑
  2. 计算机硬件实验代码转换,计算机硬件实验指导书(17页)-原创力文档
  3. java 子类中this,请问子类中的构造方法中 this(name,beijing,school);是啥意思
  4. 深度学习总结:GAN,3种方式实现fixedGtrainD,fixedDtrainG, retain, detach
  5. 树形结构:从二分查找,二叉搜索树寻找最近祖先,从递归到迭代,实现技巧总结
  6. RabbitMq--2--安装
  7. Web安全学习 Week1
  8. CSS 实例之翻转图片
  9. Cache-Control常用类型
  10. 创建自己的Vagrant box