系列文章目录


前言

目录

系列文章目录

前言

一、简介

1.1 快速入门

1.2 背景

1.3 动机

1.4 设计

1.5 参考资料:

1.6 替代方案

二、行为

2.1 骨架

2.2 生命周期

2.3 初始化

2.4 状态

2.5 反馈消息

2.6 日志记录器

2.7 复杂示例

三、复合体

3.1 选择器

3.2 序列

3.3 并行


一、简介

1.1 快速入门

        若想直接体验实践操作,可浏览演示项目,或阅读ROS2机器人教程——该教程将逐步构建一个针对机器人场景的复杂行为树(无需ROS2知识基础)。

1.2 背景

注释

        行为树是一种决策引擎,常用于游戏和机器人行业。

        其他决策引擎包括分层有限状态机、任务网络和脚本引擎,它们各有优劣。行为树处于这些引擎的中间位置,既能实现有目的的规划以达成目标,又具备足够的反应能力,可在重要事件发生时灵活调整。核心特性:

  • 时钟机制 - 无需多线程即可实现执行间隙的任务处理
  • 优先级处理 - 自然支持高优先级中断的切换机制
  • 结构简洁 - 核心组件极少,便于设计师操作
  • 可扩展性 - 节点增加时不会遭遇组合爆炸(不同于状态机)
  • 动态性 - 可在时钟周期间或父行为本身中实时修改行为树结构

        部分文献中将“优先级处理”称为“响应性”。现有大量行为树相关资料可供参考,建议从以下内容入手:

1.3 动机

        推动py_trees早期发展的应用场景是机器人技术,特别是单个机器人的高层决策——即场景/应用层。例如,使机器人能够在建筑物内导航送达包裹并安全返回基地的场景。

        开发范围涵盖所有无需低延迟响应的决策(如反应式安全控制),包括:对接/分离流程、初始定位舞步、拓扑路径规划、导航上下文切换、LED与声音交互、电梯进出决策。

        需求驱动因素还包括:需将场景开发任务下放给非控制工程师(初级工程师、实习生、软件工程师),并确保其能以最快速度完成开发与调试。

        在尝试有限状态机后,随着问题规模扩大导致接线复杂度激增,行为树最终成为完美解决方案。

1.4 设计

        先前讨论的机器人应用场景需求与更普遍的情况相符:

注意

        快速开发无需实时响应的中型决策引擎。

        快速开发:选择Python作为首选语言,因其能实现更快的开发周期及更短的学习曲线(若需将C++控制工程师的工作负担转移给初级/实习/软件工程师,此点至关重要)。

        中等规模:单机器人场景的行为数量通常最多仅达数百种。这与游戏NPC截然不同——后者需执行数千种行为及/或决策树,常面临规模扩展难题。此特性往往影响语言选择(C++)及决策树设计。我们的需求相对精简,因此设计更具灵活性,例如采用Python作为首选语言。

        非实时响应:若需低延迟控制措施(尤其涉及安全场景),最佳方案是直接在控制层处理,或更优地采用嵌入式处理。这与人类神经系统的运作原理相似。其余决策仅需约50-200毫秒延迟即可消除人类与机器人交互时感知到的明显延迟。

提示

        若需满足此要求,您可能需要采用C++实现方案,通过按需(非周期性)触发时钟机制将延迟降至最低。

        Python实现:本方案运用了Python所有炫酷技巧(生成器、装饰器)。为构建实用、易用(且无锁)的框架,设计中假设了以下约束:

  • 树实例间不交互或共享数据
  • 树执行不并行化(每次仅初始化或执行单一行为)

1.5 参考资料:

1.6 替代方案

二、行为

        行为是行为树中最小的元素,即叶节点。行为通常代表两种操作:检查(我饿了吗?)或执行(买些巧克力饼干)。

2.1 骨架

        在py_trees中,行为通过继承Behaviour类创建。骨架示例如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Example showing how to create a skeleton behaviour."""

import random
import typing

import py_trees


class Foo(py_trees.behaviour.Behaviour):
    """A skeleton behaviour that inherits from the PyTrees Behaviour class."""

    def __init__(self, name: str) -> None:
        """
    最小化一次性初始化。

    一个经验法则是:仅包含与将该行为插入树结构进行离线渲染为点图相关的初始化操作。

    其他一次性初始化需求应通过setup()方法实现。
        """
        super(Foo, self).__init__(name)

    def setup(self, **kwargs: typing.Any) -> None:
        """
        最小化设置实现。

        何时调用此函数?
          该函数应由程序手动调用以单独设置此行为,或更常见地通过 :meth:`~py_trees.behaviour. Behaviour.setup_with_descendants` 或  :meth:`~py_trees.trees.BehaviourTree.setup` 调用,这两种方式都会遍历该行为及其子节点(包括子节点的子节点...),依次对每个节点调用 :meth:`~py_trees.behaviour.Behaviour.setup`。

          若行为执行成功需依赖关键初始化操作,请在`:meth:`~py_trees.behaviour.Behaviour.initialise`方法中添加守护逻辑,防止未完成设置时被调用。

        此处应如何处理?
          延迟执行一次性初始化操作,避免干扰行为在树结构中离线渲染为点图的过程,或影响行为配置的验证。

          典型示例包括:

          - 硬件或驱动程序初始化
          - 中间件初始化(如ROS发布者/订阅者/服务)
          - 在子节点添加或移除后并行检查策略配置有效性
        """
        self.logger.debug("  %s [Foo::setup()]" % self.name)

    def initialise(self) -> None:
        """
        最小化初始化实现。

        何时调用此方法?
          当行为首次被触发时,以及后续状态非运行状态时。

        此处应执行何种操作?
          在行为开始工作前所需的任何初始化操作。
        """
        self.logger.debug("  %s [Foo::initialise()]" % self.name)

    def update(self) -> py_trees.common.Status:
        """
        最小更新实现。

        何时调用此方法?
          每次行为被触发时。

        此处需执行哪些操作?
          - 触发、检查、监控。任何操作...但切勿阻塞!
          - 设置反馈消息
          - 返回py_trees.common.Status.[RUNNING, SUCCESS, FAILURE]
        """
        self.logger.debug("  %s [Foo::update()]" % self.name)
        ready_to_make_a_decision = random.choice([True, False])
        decision = random.choice([True, False])
        if not ready_to_make_a_decision:
            return py_trees.common.Status.RUNNING
        elif decision:
            self.feedback_message = "We are not bar!"
            return py_trees.common.Status.SUCCESS
        else:
            self.feedback_message = "Uh oh"
            return py_trees.common.Status.FAILURE

    def terminate(self, new_status: py_trees.common.Status) -> None:
        """
        最小终止实现。

        何时调用此方法?
           当行为切换至非运行状态时。
            - SUCCESS || FAILURE:行为的工作周期已结束
            - INVALID:更高优先级的分支已中断,或正在关闭
        """
        self.logger.debug(
            "  %s [Foo::terminate().terminate()][%s->%s]"
            % (self.name, self.status, new_status)
        )

2.2 生命周期

        要了解该机制的实际运行效果,可运行py-trees-demo-behaviour-lifecycle程序(点击链接查看详情并获取源代码):

        需要重点关注的要点:

  • initialise()方法仅在行为尚未运行时才会触发
  • 父级tick()方法负责决定何时调用initialise()、stop()和terminate()方法
  • 父级tick()方法始终会调用update()方法
  • update()方法负责决定行为的状态

2.3 初始化

由于初始化至少涉及三种方法,可能难以确定初始化代码应置于何处。

注意

        __init__方法应充分实例化行为,以支持离线生成dot图。

        后续我们将探讨如何将行为树可视化为dot图。当前只需理解:需保持初始化代码足够精简,以便通过持续集成服务器(如Jenkins)生成行为树的dot图。此功能具有重要实用价值。

  • 不包含可能缺失的硬件连接,例如USB激光雷达
  • 不包含可能缺失的中间件连接,例如ROS发布者/订阅者/服务
  • 无需启动其他冗余资源,例如后台重型线程

注意

        setup负责处理执行所需的所有其他一次性资源初始化

        本质上涵盖构造函数未处理的所有内容——硬件连接、中间件及其他重型资源。

注意

        initialise用于配置并重置行为,以准备(重复)执行

        此处的初始化旨在为任务即时执行做好准备。示例包括:

  • 初始化/重置/清除变量
  • 启动计时器
  • 即时发现并建立中间件连接
  • 向系统其他位置发送启动控制器运行的指令
  • ...

2.4 状态

行为中最关键的部分是在update()方法中确定行为的状态。该状态将决定后续行为树中应沿哪个方向继续执行。虽然我们尚未涉及行为树,但正是这一机制驱动着行为树的决策过程。

classpy_trees.common.Status(value)[source]

        行为状态的枚举类型,用于表示行为的状态。

  • FAILURE= ‘FAILURE’
    • 行为检查失败,或其动作执行结果为失败。
  • INVALID= ‘INVALID’
    • 行为未初始化和/或处于非活动状态,即当前未被触发。
  • RUNNING= ‘RUNNING’
    • 行为正在执行某项动作,结果尚未确定。
  • SUCCESS= ‘SUCCESS’
    • 行为检查通过,或其动作执行已成功完成。

        update()方法必须返回RUNNING、SUCCESS或FAILURE之一。INVALID状态为初始默认值,通常由其他机制自动设置(例如当更高优先级行为取消当前选定行为时)。

2.5 反馈消息

        description=description(),
        epilog=epilog(),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

        行为具有内置的反馈消息机制,可在initialise()或terminate()方法中清除,并在update()方法中更新。

提示

        当发生重大事件时修改反馈消息。

        反馈消息旨在协助在重大事件发生时通知人类,或用于决定何时记录树的状态。若每帧都进行通知或记录,最终将陷入海量数据的噪音筛选困境——在大量无实质变化的数据中,难以定位引发意外或灾难性行为的关键节点。

        通常在RUNNING状态发生重大事件时,或需提供结果关联信息(如失败原因)时,设置反馈消息尤为重要。

        示例:负责角色动作规划的行为体长期处于运行状态。应避免在每次 tick 周期都更新反馈消息并附带最新计划细节,而应仅在发生重大变更时更新消息——例如当前计划被重新规划或被抢占时。

2.6 日志记录器

        这些在演示程序中广泛使用。它们仅适用于调试简单示例,不适用于更复杂的场景。此类日志记录往往较为冗余,需要大量过滤才能找到您关注的变更点(参见上文关于反馈消息的说明)。

2.7 复杂示例

        py-trees-demo-action-behaviour 程序演示了更为复杂的行为,它体现了上述若干概念,而这些概念在极其简单的生命周期计数器行为中并未体现。

  • 在设置方法中模拟外部进程并建立连接
  • 在初始化方法中通过外部进程启动新目标
  • 在更新方法中监控进行中的目标状态
  • 根据外部进程反馈判定运行状态为 RUNNING/SUCCESS

注意

        行为的 update() 方法永不阻塞,最多仅监控进度并通过将状态设为 RUNNING 暂停行为触发树所需的决策。尽管可能造成混淆,这通常被称为阻塞行为。

三、复合体

        行为树中的复合体(多子节点)类型。

        复合体负责在给定步长(执行)中引导树的遍历路径。它们是行为树的工厂(序列和并行)和决策者(选择器)。

PyTree复合

        复合行为通常用于管理子节点,并对它们的执行方式及结果返回应用某些逻辑,但自身通常不执行任何操作。所需的检查或操作应在非复合行为中完成。

        几乎所有所需功能都可以通过组合这三种复合行为来实现。事实上,正是这一特性使行为树如此具有吸引力——它将复杂的决策逻辑分解为仅三个基本元素。虽然可以(且通常需要)通过自定义复合行为扩展该体系,但请慎重考虑:绝大多数情况下,现有复合行为的组合已能满足需求,过度扩展只会增加行为树逻辑的内在复杂性。这使得行为树的设计、内省和调试变得异常困难。例如,设计会议通常围绕白板上绘制的草图展开。当这些图仅由五个元素(选择器、序列、并行、装饰器和行为)构成时,逻辑一目了然。但当基础元素数量翻倍时,你可能就不得不回到终端解析代码的状态了。

提示

        您永远无需子类化或创建新的组合器。

        本库中三种组合器的基本操作模式如下:

  • 选择器:根据级联优先级执行子节点
  • 序列:顺序执行子节点
  • 并行器:并发执行子节点

        本库在不破坏各组合器基本特性(如上所述)的前提下,为其实现方式提供一定灵活性。选择器和序列器可配置为带/不带内存(子节点运行时支持恢复/重置),并行器可配置为等待所有子节点完成、任一子节点成功、全部子节点成功或部分子节点成功。

提示

        请通过每个组合文档中的链接访问相应的演示程序。

3.1 选择器

classpy_trees.composites.Selector(name: strmemory: boolchildren: Optional[Sequence[Behaviour]] = None)[source]

        选择器是决策者。

        选择器依次执行其每个子行为,直至其中一个成功(此时选择器自身返回 RUNNING 或 SUCCESS),或执行完所有子行为(此时选择器自身返回 FAILURE)。我们通常将选择子行为视为在优先级之间进行抉择的手段。每个子行为及其子树代表优先级递减的路径。

注意

        从低优先级分支切换到高优先级分支时,会向先前执行的低优先级分支发送停止(INVALID)信号。该信号将沿该子进程自身的子树向下传播。行为应确保捕获此信号并进行适当的销毁操作。

注意

        若配置了内存机制,当子节点在前次时钟周期内返回运行状态时,将跳过更高优先级的检查。即优先级一旦锁定,该节点将运行至完成,除非选择器在树中其他位置被更高优先级中断,否则无法中断。

另请参阅

        py-trees-demo-selector 程序演示了选择器下的更高优先级切换机制。

参数

  • memory (bool) – 若上个时段处于 RUNNING 状态,则与 RUNNING 子行为一起恢复
  • name (str) – 组合行为名称
  • children ([Behaviour]) – 要添加的子行为列表

3.2 序列

classpy_trees.composites.Sequence(name: strmemory: boolchildren: Optional[Sequence[Behaviour]] = None)[source]

        序列是行为树的流水线。

        只要每个子任务返回成功状态,序列就会依次处理所有子任务。若任何子任务返回失败或运行中状态,序列将立即终止,父任务将采用该子任务的结果。当序列到达最后一个子任务时,无论其状态如何,序列都将返回该子任务的结果。

注意

        序列在遇到运行中的子任务时即告终止,后续行为将不再执行。

注意

        若启用内存功能,且前次执行时子节点处于运行状态,序列将直接跳转至运行行为,跳过所有先前行为。启用内存功能有助于处理长期运行的任务序列;禁用内存功能则适用于需要在工作开始前设置条件检查的情况。

另请参阅

        py-trees-demo-sequence 程序演示了简单序列的运行过程。

参数

  • name (字符串) – 复合行为名称
  • memory (布尔值) – 若上个时段处于运行状态,则使用运行中的子行为继续执行
  • children (可选[序列[行为]] – 要添加的子行为列表

3.3 并行

classpy_trees.composites.Parallel(name: strpolicy: Basechildren: Optional[Sequence[Behaviour]] = None)[source]

        并行关系促成了一种超现实的远程并行性。

        每次并行操作被触发时,其所有子节点都会被同步处理。然而这种并行性仅是概念层面的——子节点实际仍按顺序被处理,但在树结构和并行操作的视角下,所有子节点都呈现出同时被处理的效果。

        这种并行性也不意味着会启动多个线程或进程来执行工作。某些行为可能在后台启动线程或进程,或连接到现有线程/进程。但行为本身仅负责监控这些进程,且被封装在py_tree中——该树始终以单线程操作进行计时。

  • 若任何子节点返回失败,并行操作将返回失败
  • 采用SuccessOnAll策略的并行操作仅在所有子进程返回SUCCESS时返回SUCCESS
  • 采用SuccessOnOne策略的并行操作,只要至少一个子进程返回SUCCESS且其余处于RUNNING状态即返回SUCCESS
  • 采用SuccessOnSelected策略的并行操作,仅当指定子集子进程返回SUCCESS时返回SUCCESS

        SuccessOnAll与SuccessOnSelected策略可配置为同步模式,此时标记为SUCCESS的子进程在后续计时周期中将被跳过,直至满足策略条件或存在子进程返回FAILURE状态。

        采用SuccessOnSelected策略的并行操作将在setup()和tick()方法中双重验证:所选子进程集必须是当前并行操作子进程的子集。

另请参阅

        上下文切换演示

参数

  • name (字符串) –
  • policy (基类) –
  • children (可选[序列[行为]]) –
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐