目录
目录README.md

1、行为树可视化编辑器 2、行为树驱动的pacman游戏 3、pacman行为树xml

1. main.py

  • 功能: 主程序入口,负责初始化应用程序并启动主窗口。

  • 内容:

    import tkinter as tk
    from behavior_tree_editor import BehaviorTreeEditor
    
    if __name__ == "__main__":
        root = tk.Tk()
        app = BehaviorTreeEditor(root)
        root.mainloop()

2. behavior_tree_editor.py

  • 功能: 包含 BehaviorTreeEditor 类的定义,负责应用程序的整体结构和初始化。

  • 内容:

    import tkinter as tk
    from tkinter import messagebox, simpledialog, filedialog
    from node_operations import NodeOperations
    from canvas_operations import CanvasOperations
    from connection_operations import ConnectionOperations
    from layout_operations import LayoutOperations
    from xml_operations import XmlOperations
    from PIL import Image, ImageTk
    
    class BehaviorTreeEditor(NodeOperations, CanvasOperations, ConnectionOperations, LayoutOperations, XmlOperations):
        def __init__(self, root):
            self.root = root
            self.root.title("Behavior Tree Editor")
            self.canvas = tk.Canvas(root, bg="white")
            self.canvas.pack(fill=tk.BOTH, expand=True)
    
            self.nodes = []
            self.selected_node = None
            self.drag_data = {"x": 0, "y": 0, "item": None}
            self.scale = 1.0
            self.connecting = False
            self.connection_start = None
    
            self.create_toolbar()
            self.setup_bindings()
            self.load_icon()
    
        def create_toolbar(self):
            toolbar = tk.Frame(self.root)
            toolbar.pack(side=tk.TOP, fill=tk.X)
    
            node_types = ["Sequence", "Parallel", "Fallback", "Condition", "Action"]
            for node_type in node_types:
                tk.Button(toolbar, text=node_type, command=lambda t=node_type: self.add_node(t)).pack(side=tk.LEFT)
    
            tk.Button(toolbar, text="Export XML", command=self.export_xml).pack(side=tk.LEFT)
            tk.Button(toolbar, text="Import XML", command=self.import_xml).pack(side=tk.LEFT)
            tk.Button(toolbar, text="Auto Layout", command=self.auto_layout).pack(side=tk.LEFT)
    
        def setup_bindings(self):
            self.canvas.bind("<Button-1>", self.on_canvas_click)
            self.canvas.bind("<B1-Motion>", self.on_drag)
            self.canvas.bind("<ButtonRelease-1>", self.on_release)
            self.canvas.bind("<Button-3>", self.on_right_click)
            self.canvas.bind("<MouseWheel>", self.on_mousewheel)
    
        def load_icon(self):
            icon_path = "icon/eddie.png"  # 替换为你的图标路径
            icon = Image.open(icon_path)
            icon = icon.resize((32, 32), Image.ANTIALIAS)  # 调整图标大小
            self.icon = ImageTk.PhotoImage(icon)
            self.root.iconphoto(True, self.icon)

3. node_operations.py

  • 功能: 包含与节点操作相关的函数,如添加、删除、重命名、编辑节点属性等。

  • 内容:

    import tkinter as tk
    from tkinter import simpledialog
    
    class NodeOperations:
        def add_node(self, node_type):
            node = {
                "type": node_type,
                "x": 50,
                "y": 50,
                "children": [],
                "name": node_type,
                "class": f"{node_type}Node",
                "instance_name": node_type,
                "input": " ",
                "output": " "
            }
            self.nodes.append(node)
            self.draw_node(node)
    
        def draw_node(self, node):
            x, y = node["x"], node["y"]
            color = "lightblue" if node["type"] in ["Sequence", "Parallel", "Fallback"] else "lightgreen"
            scaled_width = 100 * self.scale
            scaled_height = 40 * self.scale
            node["id"] = self.canvas.create_rectangle(x-scaled_width/2, y-scaled_height/2, x+scaled_width/2, y+scaled_height/2, fill=color, tags="node")
            node["text_id"] = self.canvas.create_text(x, y, text=f"{node['type']}: {node['name']}", font=("Arial", int(10*self.scale)), tags="node")
            self.canvas.tag_bind(node["id"], "<Button-1>", lambda e, n=node: self.select_node(n))
            self.canvas.tag_bind(node["text_id"], "<Button-1>", lambda e, n=node: self.select_node(n))
    
        def select_node(self, node):
            if self.connecting:
                self.finish_connecting(node)
            else:
                self.selected_node = node
    
        def delete_node(self, node):
            self.canvas.delete(node["id"])
            self.canvas.delete(node["text_id"])
            self.nodes.remove(node)
            for parent in self.nodes:
                if node in parent["children"]:
                    parent["children"].remove(node)
            self.redraw_edges()
    
        def rename_node(self, node):
            new_name = simpledialog.askstring("Rename", "Enter new name:", initialvalue=node["name"])
            if new_name:
                node["name"] = new_name
                node["instance_name"] = new_name
                self.canvas.itemconfig(node["text_id"], text=f"{node['type']}: {node['name']}")
    
        def edit_node_properties(self, node):
            dialog = tk.Toplevel(self.root)
            dialog.title(f"Edit {node['type']} Properties")
    
            tk.Label(dialog, text="Class:").grid(row=0, column=0)
            class_entry = tk.Entry(dialog)
            class_entry.insert(0, node.get("class", ""))
            class_entry.grid(row=0, column=1)
    
            tk.Label(dialog, text="Instance Name:").grid(row=1, column=0)
            instance_name_entry = tk.Entry(dialog)
            instance_name_entry.insert(0, node.get("instance_name", ""))
            instance_name_entry.grid(row=1, column=1)
    
            tk.Label(dialog, text="Input:").grid(row=2, column=0)
            input_entry = tk.Entry(dialog)
            input_entry.insert(0, node.get("input", ""))
            input_entry.grid(row=2, column=1)
    
            tk.Label(dialog, text="Output:").grid(row=3, column=0)
            output_entry = tk.Entry(dialog)
            output_entry.insert(0, node.get("output", ""))
            output_entry.grid(row=3, column=1)
    
            def save_properties():
                node["class"] = class_entry.get()
                node["instance_name"] = instance_name_entry.get()
                node["input"] = input_entry.get()
                node["output"] = output_entry.get()
                dialog.destroy()
    
            tk.Button(dialog, text="Save", command=save_properties).grid(row=4, column=0, columnspan=2)

4. canvas_operations.py

  • 功能: 包含与画布操作相关的函数,如点击、拖动、释放、右键点击、缩放等。

  • 内容:

    import tkinter as tk
    
    class CanvasOperations:
        def on_canvas_click(self, event):
            item = self.canvas.find_withtag("current")
            if item:
                node = next((n for n in self.nodes if n["id"] == item[0] or n["text_id"] == item[0]), None)
                if node:
                    if self.connecting:
                        self.finish_connecting(node)
                    else:
                        self.drag_data["item"] = item
                        self.drag_data["x"] = event.x
                        self.drag_data["y"] = event.y
    
        def on_drag(self, event):
            if self.drag_data["item"]:
                dx = event.x - self.drag_data["x"]
                dy = event.y - self.drag_data["y"]
                self.canvas.move("current", dx, dy)
                self.drag_data["x"] = event.x
                self.drag_data["y"] = event.y
                self.update_node_position(self.drag_data["item"][0], dx, dy)
                self.redraw_edges()
    
        def update_node_position(self, item_id, dx, dy):
            for node in self.nodes:
                if node["id"] == item_id or node["text_id"] == item_id:
                    node["x"] += dx
                    node["y"] += dy
                    self.canvas.coords(node["id"], 
                        node["x"]-50*self.scale, node["y"]-20*self.scale, 
                        node["x"]+50*self.scale, node["y"]+20*self.scale)
                    self.canvas.coords(node["text_id"], node["x"], node["y"])
                    break
    
        def redraw_edges(self):
            self.canvas.delete("edge")
            for node in self.nodes:
                for child in node["children"]:
                    self.draw_edge(node, child)
    
        def on_release(self, event):
            self.drag_data["item"] = None
    
        def on_right_click(self, event):
            item = self.canvas.find_withtag("current")
            if item:
                node = next((n for n in self.nodes if n["id"] == item[0] or n["text_id"] == item[0]), None)
                if node:
                    self.show_context_menu(event, node)
    
        def show_context_menu(self, event, node):
            context_menu = tk.Menu(self.root, tearoff=0)
            context_menu.add_command(label="Connect", command=lambda: self.start_connecting(node))
            context_menu.add_command(label="Rename", command=lambda: self.rename_node(node))
            context_menu.add_command(label="Edit Properties", command=lambda: self.edit_node_properties(node))
            context_menu.add_command(label="Delete", command=lambda: self.delete_node(node))
            context_menu.post(event.x_root, event.y_root)
    
        def on_mousewheel(self, event):
            # Zoom in/out with mouse wheel
            if event.delta > 0:
                self.scale *= 1.1
            else:
                self.scale /= 1.1
            self.scale = max(0.1, min(self.scale, 5.0))  # Limit scale between 0.1 and 5.0
            self.redraw_all()

5. connection_operations.py

  • 功能: 包含与节点连接相关的函数,如开始连接、完成连接、绘制连接线等。

  • 内容:

    import tkinter as tk
    from tkinter import messagebox
    
    class ConnectionOperations:
        def start_connecting(self, node):
            self.connecting = True
            self.connection_start = node
            self.root.config(cursor="cross")
    
        def finish_connecting(self, target_node):
            if self.connection_start and self.connection_start != target_node:
                if self.is_valid_connection(self.connection_start, target_node):
                    if target_node not in self.connection_start["children"]:
                        self.connection_start["children"].append(target_node)
                    self.redraw_edges()
                else:
                    messagebox.showerror("Invalid Connection", "Cannot create a cycle in the tree structure.")
            self.root.config(cursor="")
            self.connecting = False
            self.connection_start = None
    
        def is_valid_connection(self, parent, child):
            if child in parent["children"]:
                return False
            if self.is_ancestor(child, parent):
                return False
            return True
    
        def is_ancestor(self, node, potential_descendant):
            if not node["children"]:
                return False
            if potential_descendant in node["children"]:
                return True
            return any(self.is_ancestor(child, potential_descendant) for child in node["children"])
    
        def draw_edge(self, parent, child):
            start_x, start_y = parent["x"], parent["y"] + 20 * self.scale
            end_x, end_y = child["x"], child["y"] - 20 * self.scale
            mid_y = (start_y + end_y) / 2
            self.canvas.create_line(start_x, start_y, start_x, mid_y, end_x, mid_y, end_x, end_y, smooth=True, arrow=tk.LAST, tags="edge", width=self.scale)

6. layout_operations.py

  • 功能: 包含与自动布局相关的函数,如自动布局、子树布局等。

  • 内容:

    class LayoutOperations:
        def auto_layout(self):
            if not self.nodes:
                return
    
            root_nodes = [node for node in self.nodes if not any(node in parent["children"] for parent in self.nodes)]
            
            canvas_width = self.canvas.winfo_width()
            canvas_height = self.canvas.winfo_height()
            
            x_spacing = canvas_width / (len(root_nodes) + 1)
            y_spacing = 100 * self.scale
            
            for i, root_node in enumerate(root_nodes):
                self.layout_subtree(root_node, (i + 1) * x_spacing, 50, x_spacing, y_spacing)
            
            self.redraw_all()
    
        def layout_subtree(self, node, x, y, x_spacing, y_spacing):
            node["x"], node["y"] = x, y
            
            if not node["children"]:
                return x
            
            total_width = (len(node["children"]) - 1) * x_spacing
            start_x = x - total_width / 2
            
            for child in node["children"]:
                child_x = self.layout_subtree(child, start_x, y + y_spacing, x_spacing / 2, y_spacing)
                start_x += x_spacing
            
            return x
    
        def redraw_all(self):
            self.canvas.delete("all")
            for node in self.nodes:
                self.draw_node(node)
            self.redraw_edges()

7. xml_operations.py

  • 功能: 包含与XML导入导出相关的函数,如导出XML、导入XML、解析XML节点等。

  • 内容:

    import xml.etree.ElementTree as ET
    import xml.dom.minidom
    from tkinter import filedialog, messagebox
    
    class XmlOperations:
        def export_xml(self):
            root = ET.Element("BehaviorTree")
            for node in self.nodes:
                if not any(child for n in self.nodes for child in n["children"] if child == node):
                    self.add_xml_node(root, node)
            tree = ET.ElementTree(root)
            file_path = filedialog.asksaveasfilename(defaultextension=".xml")
            if file_path:
                xml_string = ET.tostring(root, encoding="unicode")
                xml_string = xml_string.replace('<BehaviorTree>', '').replace('</BehaviorTree>', '')
                dom = xml.dom.minidom.parseString(xml_string)
                pretty_xml = dom.toprettyxml(indent="  ")
                pretty_xml = pretty_xml.replace('<?xml version="1.0" ?>', '')
                pretty_xml = pretty_xml.strip()  # Remove leading and trailing whitespace
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(pretty_xml)
                messagebox.showinfo("Export", f"Behavior Tree exported to {file_path}")
    
        def add_xml_node(self, parent, node):
            xml_node = ET.SubElement(parent, node["type"])
            xml_node.set("class", node["class"])
            xml_node.set("instance_name", node["instance_name"])
            xml_node.set("input", node["input"])
            xml_node.set("output", node["output"])
            for child in node["children"]:
                self.add_xml_node(xml_node, child)
    
        def import_xml(self):
            file_path = filedialog.askopenfilename(filetypes=[("XML files", "*.xml")])
            if file_path:
                with open(file_path, 'r', encoding='utf-8') as f:
                    xml_content = f"<BehaviorTree>{f.read()}</BehaviorTree>"
                root = ET.fromstring(xml_content)
                self.canvas.delete("all")
                self.nodes = []
                self.parse_xml_node(root, None, 50, 50)
                self.auto_layout()
    
        def parse_xml_node(self, xml_node, parent, x, y):
            node = {
                "type": xml_node.tag,
                "name": xml_node.get("instance_name", xml_node.tag),
                "class": xml_node.get("class", f"{xml_node.tag}Node"),
                "instance_name": xml_node.get("instance_name", xml_node.tag),
                "input": xml_node.get("input", " "),
                "output": xml_node.get("output", " "),
                "x": x,
                "y": y,
                "children": []
            }
            self.nodes.append(node)
            self.draw_node(node)
            if parent:
                parent["children"].append(node)
                self.draw_edge(parent, node)
            
            for child_xml in xml_node:
                self.parse_xml_node(child_xml, node, x, y)
            return y
关于
82.6 MB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

©Copyright 2023 CCF 开源发展委员会
Powered by Trustie& IntelliDE 京ICP备13000930号