Matplotlib绘制游戏内树状图:如何用Python可视化你的游戏数据

引言:为什么游戏数据需要树状图可视化?
在现代游戏开发中,数据驱动的设计已经成为主流。无论是分析玩家行为、平衡游戏经济系统,还是理解游戏内部的层次结构,数据可视化都扮演着至关重要的角色。树状图(Tree Diagram)作为一种展示层次结构数据的优秀工具,在游戏开发中有着广泛的应用场景。
想象一下,你正在开发一款RPG游戏,游戏内有复杂的职业系统:战士、法师、刺客等基础职业,每个基础职业下又有多个进阶分支。或者你正在分析游戏的技能树系统,需要清晰地展示技能之间的依赖关系。在这些场景中,树状图能够直观地展示数据之间的层级关系,帮助开发者和玩家快速理解系统结构。
Matplotlib作为Python最流行的数据可视化库之一,虽然主要以绘制统计图表闻名,但通过其强大的灵活性和扩展性,我们完全可以利用它来创建精美的树状图。本文将详细介绍如何使用Matplotlib绘制游戏内的树状图,包括基础概念、实现方法、高级技巧以及实际应用案例。
树状图的基本概念与游戏应用场景
什么是树状图?
树状图(Tree Diagram)是一种用于表示层次结构数据的图形。它由节点(Node)和边(Edge)组成,其中节点表示数据实体,边表示节点之间的关系。树状图具有以下特点:
有一个根节点(Root Node),位于层次结构的顶部
每个节点可以有零个或多个子节点
除了根节点外,每个节点有且仅有一个父节点
不存在循环关系
游戏中的树状图应用场景
技能树系统
:展示角色技能的解锁路径和依赖关系
科技树
:在策略游戏中展示可研发的科技及其前置条件
装备合成树
:展示装备的合成路径和材料需求
剧情分支树
:展示游戏剧情的分支选择和后果
角色职业树
:展示职业的进阶路径和分支选择
成就系统
:展示成就的解锁条件和依赖关系
使用Matplotlib绘制基础树状图
环境准备
首先,确保你已经安装了必要的库:
pip install matplotlib numpy
基础实现:手动布局节点
让我们从一个简单的例子开始,绘制一个基础的技能树:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np

def draw_basic_skill_tree():
“””
绘制一个基础的技能树示例
“””
# 创建画布
fig, ax = plt.subplots(figsize=(12, 8))
ax.set_aspect(‘equal’)

# 定义节点位置 (x, y)
# 根节点
root_pos = (0, 0)
# 第一层子节点
child1_pos = (-2, -2)
child2_pos = (0, -2)
child3_pos = (2, -2)
# 第二层子节点
grandchild1_pos = (-2.5, -4)
grandchild2_pos = (-1.5, -4)
grandchild3_pos = (0, -4)

# 绘制连接线
# 根节点到第一层
ax.plot([root_pos[0], child1_pos[0]], [root_pos[1], child1_pos[1]], ‘k-‘, linewidth=2)
ax.plot([root_pos[0], child2_pos[0]], [root_pos[1], child2_pos[1]], ‘k-‘, linewidth=2)
ax.plot([root_pos[0], child3_pos[0]], [root_pos[1], child3_pos[1]], ‘k-‘, linewidth=2)
# 第一层到第二层
ax.plot([child1_pos[0], grandchild1_pos[0]], [child1_pos[1], grandchild1_pos[1]], ‘k-‘, linewidth=2)
ax.plot([child1_pos[0], grandchild2_pos[0]], [child1_pos[1], grandchild2_pos[1]], ‘k-‘, linewidth=2)
ax.plot([child2_pos[0], grandchild3_pos[0]], [child2_pos[1], grandchild3_pos[1]], ‘k-‘, linewidth=2)

# 绘制节点
# 根节点 – 战士
root_circle = patches.Circle(root_pos, radius=0.3, facecolor=’#FF6B6B’, edgecolor=’black’, linewidth=2)
ax.add_patch(root_circle)
ax.text(root_pos[0], root_pos[1], ‘战士\n基础’, ha=’center’, va=’center’,
fontsize=10, fontweight=’bold’, color=’white’)

# 第一层子节点
# 狂战士
circle1 = patches.Circle(child1_pos, radius=0.25, facecolor=’#4ECDC4′, edgecolor=’black’, linewidth=2)
ax.add_patch(circle1)
ax.text(child1_pos[0], child1_pos[1], ‘狂战士’, ha=’center’, va=’center’,
fontsize=9, fontweight=’bold’, color=’white’)

# 剑圣
circle2 = patches.Circle(child2_pos, radius=0.25, facecolor=’#45B7D1′, edgecolor=’black’, linewidth=2)
ax.add_patch(circle2)
ax.text(child2_pos[0], child2_pos[1], ‘剑圣’, ha=’center’, va=’center’,
fontsize=9, fontweight=’bold’, color=’white’)

# 盾卫
circle3 = patches.Circle(child3_pos, radius=0.25, facecolor=’#96CEB4′, edgecolor=’black’, linewidth=2)
ax.add_patch(circle3)
ax.text(child3_pos[0], child3_pos[1], ‘盾卫’, ha=’center’, va=’center’,
fontsize=9, fontweight=’bold’, color=’white’)

# 第二层子节点
# 狂暴
gc1 = patches.Circle(grandchild1_pos, radius=0.2, facecolor=’#FFEAA7′, edgecolor=’black’, linewidth=2)
ax.add_patch(gc1)
ax.text(grandchild1_pos[0], grandchild1_pos[1], ‘狂暴’, ha=’center’, va=’center’,
fontsize=8, fontweight=’bold’, color=’black’)

# 斩杀
gc2 = patches.Circle(grandchild2_pos, radius=0.2, facecolor=’#FFEAA7′, edgecolor=’black’, linewidth=2)
ax.add_patch(gc2)
ax.text(grandchild2_pos[0], grandchild2_pos[1], ‘斩杀’, ha=’center’, va=’center’,
fontsize=8, fontweight=’bold’, color=’black’)

# 神佑
gc3 = patches.Circle(grandchild3_pos, radius=0.2, facecolor=’#FFEAA7′, edgecolor=’black’, linewidth=2)
ax.add_patch(gc3)
ax.text(grandchild3_pos[0], grandchild3_pos[1], ‘神佑’, ha=’center’, va=’center’,
fontsize=8, fontweight=’bold’, color=’black’)

# 设置标题和样式
ax.set_title(‘战士技能树’, fontsize=16, fontweight=’bold’, pad=20)
ax.set_xlim(-4, 4)
ax.set_ylim(-5, 1)
ax.axis(‘off’)

plt.tight_layout()
plt.show()

# 运行示例
draw_basic_skill_tree()
这个例子展示了如何手动创建一个简单的技能树。我们使用
matplotlib.patches.Circle
来绘制圆形节点,使用
ax.plot
来绘制连接线。虽然这种方法可行,但对于复杂的树结构来说,手动计算每个节点的位置会非常繁琐。
自动化布局算法
算法原理
为了处理更复杂的树结构,我们需要实现自动布局算法。常见的树布局算法包括:
分层布局(Layered Layout)
:将树的每一层放置在同一水平线上
Reingold-Tilford算法
:一种经典的树布局算法,能够紧凑地排列节点
力导向布局(Force-directed Layout)
:模拟物理系统,使节点自然分布
我们将实现一个基于分层布局的算法,它简单且适用于大多数游戏树状图。
实现自动布局的树状图
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from collections import deque, defaultdict
import numpy as np

class TreeNode:
“””树节点类”””
def __init__(self, name, value=None, color=’#6C5CE7′):
self.name = name
self.value = value
self.color = color
self.children = []
self.parent = None
self.x = 0
self.y = 0
self.width = 1.0 # 节点宽度
self.height = 0.5 # 节点高度

def add_child(self, child_node):
“””添加子节点”””
child_node.parent = self
self.children.append(child_node)

def __repr__(self):
return f”TreeNode({self.name})”

class TreeLayout:
“””树布局计算器”””
def __init__(self, node_width=1.0, node_height=0.5, level_spacing=2.0, sibling_spacing=1.5):
self.node_width = node_width
self.node_height = node_height
self.level_spacing = level_spacing
self.sibling_spacing = sibling_spacing

def calculate_positions(self, root):
“””计算所有节点的位置”””
# 计算每个子树的宽度
self._calculate_subtree_widths(root)

# 计算根节点位置
root.x = 0
root.y = 0

# 递归计算子节点位置
self._position_children(root)

def _calculate_subtree_widths(self, node):
“””计算以该节点为根的子树宽度”””
if not node.children:
node.subtree_width = self.node_width
return node.subtree_width

total_width = 0
for child in node.children:
total_width += self._calculate_subtree_widths(child)

# 添加兄弟节点之间的间距
total_width += (len(node.children) – 1) * self.sibling_spacing
node.subtree_width = total_width
return total_width

def _position_children(self, parent):
“””计算子节点的位置”””
if not parent.children:
return

# 计算第一个子节点的起始x坐标
total_width = sum(child.subtree_width for child in parent.children)
total_width += (len(parent.children) – 1) * self.sibling_spacing

current_x = parent.x – total_width / 2

for child in parent.children:
# 设置子节点的x坐标(子树中心)
child.x = current_x + child.subtree_width / 2
# 设置子节点的y坐标(向下一层)
child.y = parent.y – self.level_spacing

# 递归处理子节点的子节点
self._position_children(child)

# 更新下一个子节点的起始位置
current_x += child.subtree_width + self.sibling_spacing

def draw_tree_automatically(root_node, title=”游戏技能树”):
“””
自动计算布局并绘制树状图
“””
# 计算布局
layout = TreeLayout(node_width=1.2, node_height=0.6, level_spacing=2.5, sibling_spacing=1.8)
layout.calculate_positions(root_node)

# 创建画布
fig, ax = plt.subplots(figsize=(14, 10))
ax.set_aspect(‘equal’)

# 收集所有节点用于确定绘图范围
all_nodes = []
def collect_nodes(node):
all_nodes.append(node)
for child in node.children:
collect_nodes(child)
collect_nodes(root_node)

# 绘制连接线(先绘制,这样节点会覆盖在线条上)
def draw_edges(node):
for child in node.children:
# 绘制从父节点到子节点的线条
# 使用折线:从父节点底部到子节点顶部
parent_bottom = (node.x, node.y – node.height/2)
child_top = (child.x, child.y + child.height/2)

# 绘制垂直线和水平线
mid_y = (parent_bottom[1] + child_top[1]) / 2
ax.plot([parent_bottom[0], parent_bottom[0]], [parent_bottom[1], mid_y],
color=’gray’, linewidth=2, alpha=0.6)
ax.plot([parent_bottom[0], child_top[0]], [mid_y, mid_y],
color=’gray’, linewidth=2, alpha=0.6)
ax.plot([child_top[0], child_top[0]], [mid_y, child_top[1]],
color=’gray’, linewidth=2, alpha=0.6)

# 递归绘制子节点的边
draw_edges(child)

draw_edges(root_node)

# 绘制节点
def draw_nodes(node):
# 创建矩形节点
rect = patches.Rectangle(
(node.x – node.width/2, node.y – node.height/2),
node.width, node.height,
facecolor=node.color,
edgecolor=’black’,
linewidth=2,
alpha=0.9,
zorder=10
)
ax.add_patch(rect)

# 添加文本
ax.text(node.x, node.y, node.name,
ha=’center’, va=’center’,
fontsize=10, fontweight=’bold’,
color=’white’ if node.color not in [‘#FFEAA7’, ‘#FDCB6E’] else ‘black’,
zorder=11)

# 如果有数值,显示在下方
if node.value is not None:
ax.text(node.x, node.y – node.height/2 – 0.3, str(node.value),
ha=’center’, va=’top’,
fontsize=8, color=’gray’, zorder=11)

# 递归绘制子节点
for child in node.children:
draw_nodes(child)

draw_nodes(root_node)

# 设置绘图范围
x_coords = [n.x for n in all_nodes]
y_coords = [n.y for n in all_nodes]

margin_x = 3
margin_y = 2

ax.set_xlim(min(x_coords) – margin_x, max(x_coords) + margin_x)
ax.set_ylim(min(y_coords) – margin_y, max(y_coords) + margin_y)

# 设置标题和样式
ax.set_title(title, fontsize=18, fontweight=’bold’, pad=20)
ax.axis(‘off’)

# 添加图例
legend_elements = [
patches.Patch(facecolor=’#FF6B6B’, label=’基础职业’),
patches.Patch(facecolor=’#4ECDC4′, label=’进阶职业’),
patches.Patch(facecolor=’#FFEAA7′, label=’终极技能’)
]
ax.legend(handles=legend_elements, loc=’upper right’, bbox_to_anchor=(1.15, 1.0))

plt.tight_layout()
plt.show()

# 创建示例技能树
def create_game_skill_tree():
“””创建一个完整的游戏技能树”””
# 根节点 – 基础职业
root = TreeNode(“战士”, “Lv.1”, ‘#FF6B6B’)

# 第一层 – 进阶职业
berserker = TreeNode(“狂战士”, “Lv.10”, ‘#4ECDC4’)
swordmaster = TreeNode(“剑圣”, “Lv.10”, ‘#4ECDC4’)
guardian = TreeNode(“盾卫”, “Lv.10”, ‘#4ECDC4’)

root.add_child(berserker)
root.add_child(swordmaster)
root.add_child(guardian)

# 第二层 – 终极技能
# 狂战士分支
rage = TreeNode(“狂暴”, “Lv.20”, ‘#FFEAA7’)
execute = TreeNode(“斩杀”, “Lv.20”, ‘#FFEAA7’)
berserker.add_child(rage)
berserker.add_child(execute)

# 剑圣分支
slash = TreeNode(“剑气”, “Lv.20”, ‘#FFEAA7’)
swordmaster.add_child(slash)

# 盾卫分支
protect = TreeNode(“神佑”, “Lv.20”, ‘#FFEAA7’)
shieldbash = TreeNode(“盾击”, “Lv.20”, ‘#FFEAA7’)
guardian.add_child(protect)
guardian.add_child(shieldbash)

return root

# 运行示例
if __name__ == “__main__”:
skill_tree = create_game_skill_tree()
draw_tree_automatically(skill_tree, “游戏角色技能树”)
这个实现包含了一个完整的树状图绘制系统:
TreeNode类
:表示树的节点,包含名称、数值、颜色等属性
TreeLayout类
:负责计算节点位置,使用分层布局算法
draw_tree_automatically函数
:整合布局计算和绘图逻辑
高级技巧:交互式树状图
添加交互功能
为了让树状图更具实用性,我们可以添加交互功能,如点击节点显示详细信息、展开/折叠子树等。虽然Matplotlib本身不是专门的交互式库,但我们可以通过事件处理实现基本交互。
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.widgets import Button
import numpy as np

class InteractiveTreePlotter:
“””交互式树状图绘制器”””

def __init__(self, root_node):
self.root = root_node
self.fig, self.ax = plt.subplots(figsize=(14, 10))
self.ax.set_aspect(‘equal’)

# 存储节点信息用于交互
self.node_patches = []
self.node_texts = []
self.node_map = {} # 位置到节点的映射
self.collapsed_nodes = set() # 折叠的节点

# 当前选中的节点
self.selected_node = None

# 绘制初始树
self.draw_tree()

# 添加交互控件
self.setup_interactions()

def calculate_positions(self, root):
“””计算节点位置(复用之前的布局算法)”””
layout = TreeLayout(node_width=1.5, node_height=0.7, level_spacing=2.5, sibling_spacing=2.0)
layout.calculate_positions(root)

def draw_tree(self):
“””绘制树状图”””
self.ax.clear()
self.node_patches.clear()
self.node_texts.clear()
self.node_map.clear()

# 计算位置
self.calculate_positions(self.root)

# 绘制连接线
self.draw_edges(self.root)

# 绘制节点
self.draw_nodes(self.root)

# 设置绘图范围
self.set_plot_limits()

# 设置标题
self.ax.set_title(“交互式游戏技能树 – 点击节点查看详情”,
fontsize=16, fontweight=’bold’, pad=20)
self.ax.axis(‘off’)

# 添加说明文本
self.fig.text(0.02, 0.02,
“操作说明:\n”
“• 点击节点:查看详细信息\n”
“• 双击节点:展开/折叠子树\n”
“• 按’R’键:重置视图\n”
“• 按’Q’键:退出”,
fontsize=9, bbox=dict(boxstyle=”round,pad=0.3″, facecolor=”lightgray”, alpha=0.7))

plt.draw()

def draw_edges(self, node):
“””绘制连接线(只绘制未折叠的分支)”””
if node in self.collapsed_nodes:
return

for child in node.children:
# 计算连接点
parent_bottom = (node.x, node.y – node.height/2)
child_top = (child.x, child.y + child.height/2)

mid_y = (parent_bottom[1] + child_top[1]) / 2

# 绘制线条
self.ax.plot([parent_bottom[0], parent_bottom[0]], [parent_bottom[1], mid_y],
color=’gray’, linewidth=2, alpha=0.6, zorder=1)
self.ax.plot([parent_bottom[0], child_top[0]], [mid_y, mid_y],
color=’gray’, linewidth=2, alpha=0.6, zorder=1)
self.ax.plot([child_top[0], child_top[0]], [mid_y, child_top[1]],
color=’gray’, linewidth=2, alpha=0.6, zorder=1)

# 递归绘制子节点的边
self.draw_edges(child)

def draw_nodes(self, node):
“””绘制节点(只绘制未折叠的分支)”””
if node in self.collapsed_nodes:
return

# 创建矩形节点
rect = patches.Rectangle(
(node.x – node.width/2, node.y – node.height/2),
node.width, node.height,
facecolor=node.color,
edgecolor=’black’,
linewidth=2,
alpha=0.9,
zorder=10
)
self.ax.add_patch(rect)
self.node_patches.append(rect)

# 添加文本
text = self.ax.text(node.x, node.y, node.name,
ha=’center’, va=’center’,
fontsize=11, fontweight=’bold’,
color=’white’ if node.color not in [‘#FFEAA7’, ‘#FDCB6E’] else ‘black’,
zorder=11)
self.node_texts.append(text)

# 存储节点映射(用于点击检测)
self.node_map[(node.x, node.y)] = node

# 如果有子节点且被折叠,显示折叠标记
if node.children and node in self.collapsed_nodes:
fold_text = self.ax.text(node.x, node.y – node.height/2 – 0.3,
f”+{len(node.children)}”,
ha=’center’, va=’top’,
fontsize=10, fontweight=’bold’,
color=’red’, zorder=11)
self.node_texts.append(fold_text)

# 递归绘制子节点
for child in node.children:
self.draw_nodes(child)

def set_plot_limits(self):
“””设置绘图范围”””
all_nodes = []
def collect(node):
all_nodes.append(node)
for child in node.children:
if child not in self.collapsed_nodes:
collect(child)
collect(self.root)

if not all_nodes:
return

x_coords = [n.x for n in all_nodes]
y_coords = [n.y for n in all_nodes]

margin_x = 3
margin_y = 2

self.ax.set_xlim(min(x_coords) – margin_x, max(x_coords) + margin_x)
self.ax.set_ylim(min(y_coords) – margin_y, max(y_coords) + margin_y)

def setup_interactions(self):
“””设置交互事件”””
# 点击事件
self.fig.canvas.mpl_connect(‘button_press_event’, self.on_click)

# 键盘事件
self.fig.canvas.mpl_connect(‘key_press_event’, self.on_key_press)

# 添加控制按钮
# 重置按钮
reset_ax = self.fig.add_axes([0.82, 0.02, 0.15, 0.04])
self.reset_button = Button(reset_ax, ‘重置视图’, color=’lightblue’, hovercolor=’blue’)
self.reset_button.on_clicked(self.reset_view)

# 信息显示区域
self.info_ax = self.fig.add_axes([0.02, 0.85, 0.25, 0.12])
self.info_ax.axis(‘off’)
self.info_text = self.info_ax.text(0, 0.5, “点击节点查看详情”,
fontsize=10, va=’center’,
bbox=dict(boxstyle=”round,pad=0.5″,
facecolor=”white”,
edgecolor=”black”,
alpha=0.9))

def on_click(self, event):
“””处理鼠标点击事件”””
if event.inaxes != self.ax:
return

# 检查是否点击了节点
clicked_node = None
for (x, y), node in self.node_map.items():
# 检查点击位置是否在节点矩形内
if (abs(event.xdata – x) < node.width/2 and abs(event.ydata - y) < node.height/2): clicked_node = node break if clicked_node: if event.dblclick: # 双击:展开/折叠 self.toggle_node(clicked_node) else: # 单击:显示信息 self.show_node_info(clicked_node) def on_key_press(self, event): """处理键盘按键事件""" if event.key == 'r': # 重置视图 self.reset_view() elif event.key == 'q': # 退出 plt.close(self.fig) elif event.key == 'c': # 折叠所有子节点 self.collapse_all() elif event.key == 'e': # 展开所有子节点 self.expand_all() def toggle_node(self, node): """展开/折叠节点""" if node in self.collapsed_nodes: self.collapsed_nodes.remove(node) else: self.collapsed_nodes.add(node) self.draw_tree() def show_node_info(self, node): """显示节点详细信息""" self.selected_node = node # 构建信息文本 info_lines = [ f"节点名称: {node.name}", f"当前等级: {node.value}", f"子节点数: {len(node.children)}", "", "节点属性:" ] # 添加子节点信息 if node.children: info_lines.append("子节点:") for child in node.children: info_lines.append(f" - {child.name}") else: info_lines.append(" (无子节点)") # 更新信息显示 info_text = "\n".join(info_lines) self.info_text.set_text(info_text) # 高亮选中的节点 for patch in self.node_patches: patch.set_edgecolor('black') patch.set_linewidth(2) # 找到对应的patch并高亮 for (x, y), n in self.node_map.items(): if n == node: for patch in self.node_patches: if (abs(patch.get_x() + patch.get_width()/2 - x) < 0.01 and abs(patch.get_y() + patch.get_height()/2 - y) < 0.01): patch.set_edgecolor('yellow') patch.set_linewidth(4) break break plt.draw() def reset_view(self, event=None): """重置视图""" self.collapsed_nodes.clear() self.selected_node = None self.draw_tree() self.info_text.set_text("点击节点查看详情") plt.draw() def collapse_all(self): """折叠所有节点""" def collapse_recursive(node): if node.children: self.collapsed_nodes.add(node) for child in node.children: collapse_recursive(child) collapse_recursive(self.root) self.draw_tree() plt.draw() def expand_all(self): """展开所有节点""" self.collapsed_nodes.clear() self.draw_tree() plt.draw() # 创建更复杂的游戏数据树 def create_complex_game_tree(): """创建一个复杂的游戏数据树""" # 根节点 - 游戏核心系统 root = TreeNode("游戏核心", "v1.0", '#FF6B6B') # 第一层 - 主要系统 combat = TreeNode("战斗系统", "核心", '#4ECDC4') economy = TreeNode("经济系统", "核心", '#4ECDC4') progression = TreeNode("成长系统", "核心", '#4ECDC4') root.add_child(combat) root.add_child(economy) root.add_child(progression) # 第二层 - 子系统 # 战斗系统子模块 skills = TreeNode("技能模块", "战斗", '#45B7D1') weapons = TreeNode("武器模块", "战斗", '#45B7D1') armor = TreeNode("防具模块", "战斗", '#45B7D1') combat.add_child(skills) combat.add_child(weapons) combat.add_child(armor) # 经济系统子模块 currency = TreeNode("货币系统", "经济", '#45B7D1') trading = TreeNode("交易系统", "经济", '#45B7D1') shop = TreeNode("商店系统", "经济", '#45B7D1') economy.add_child(currency) economy.add_child(trading) economy.add_child(shop) # 成长系统子模块 level = TreeNode("等级系统", "成长", '#45B7D1') talent = TreeNode("天赋系统", "成长", '#45B7D1') achievement = TreeNode("成就系统", "成长", '#45B7D1') progression.add_child(level) progression.add_child(talent) progression.add_child(achievement) # 第三层 - 具体实现 # 技能模块的具体技能 fireball = TreeNode("火球术", "Lv.5", '#FFEAA7') icebolt = TreeNode("冰箭术", "Lv.5", '#FFEAA7') lightning = TreeNode("闪电链", "Lv.10", '#FFEAA7') skills.add_child(fireball) skills.add_child(icebolt) skills.add_child(lightning) # 武器模块的具体武器 sword = TreeNode("长剑", "Lv.1", '#FFEAA7') bow = TreeNode("长弓", "Lv.1", '#FFEAA7') staff = TreeNode("法杖", "Lv.1", '#FFEAA7') weapons.add_child(sword) weapons.add_child(bow) weapons.add_child(staff) # 货币系统的具体货币 gold = TreeNode("金币", "基础", '#FFEAA7') diamond = TreeNode("钻石", "高级", '#FFEAA7') currency.add_child(gold) currency.add_child(diamond) return root # 运行交互式示例 if __name__ == "__main__": # 创建复杂树 game_tree = create_complex_game_tree() # 创建交互式绘图 plotter = InteractiveTreePlotter(game_tree) print("交互式树状图已启动!") print("操作说明:") print("- 单击节点:查看详细信息") print("- 双击节点:展开/折叠子树") print("- 按'R'键:重置视图") print("- 按'C'键:折叠所有节点") print("- 按'E'键:展开所有节点") print("- 按'Q'键:退出") plt.show() 这个交互式实现提供了以下功能: 点击检测 :精确识别用户点击的节点 信息显示 :在侧边栏显示节点详细信息 展开/折叠 :双击节点可以展开或折叠其子树 键盘快捷键 :提供多种快捷操作 控制按钮 :提供图形界面的控制选项 游戏数据可视化实战案例 案例1:RPG游戏技能树分析 让我们创建一个更实用的案例,展示如何分析真实的技能树数据: import matplotlib.pyplot as plt import numpy as np from datetime import datetime class SkillTreeAnalyzer: """技能树分析器""" def __init__(self, root_node): self.root = root_node self.player_data = {} def load_player_progress(self, player_data): """加载玩家进度数据""" self.player_data = player_data def analyze_skill_distribution(self): """分析技能解锁分布""" stats = { 'total_skills': 0, 'unlocked_skills': 0, 'avg_depth': 0, 'most_popular_branch': None } def traverse(node, depth=0): stats['total_skills'] += 1 if node.name in self.player_data.get('unlocked_skills', []): stats['unlocked_skills'] += 1 if not node.children: return depth depths = [traverse(child, depth + 1) for child in node.children] return max(depths) max_depth = traverse(self.root) stats['avg_depth'] = max_depth # 分析最受欢迎的分支 branch_popularity = {} for child in self.root.children: count = self.count_unlocked_in_subtree(child) branch_popularity[child.name] = count if branch_popularity: stats['most_popular_branch'] = max(branch_popularity, key=branch_popularity.get) return stats def count_unlocked_in_subtree(self, node): """计算子树中已解锁的技能数量""" count = 0 if node.name in self.player_data.get('unlocked_skills', []): count += 1 for child in node.children: count += self.count_unlocked_in_subtree(child) return count def create_progress_heatmap(self): """创建进度热力图""" # 收集所有节点 all_nodes = [] def collect(node): all_nodes.append(node) for child in node.children: collect(child) collect(self.root) # 计算每个节点的"热度"(基于玩家数据) node_heat = {} for node in all_nodes: if node.name in self.player_data.get('unlocked_skills', []): node_heat[node.name] = 1.0 # 已解锁 elif self.has_unlocked_parent(node): node_heat[node.name] = 0.5 # 可解锁 else: node_heat[node.name] = 0.0 # 未解锁 return node_heat def has_unlocked_parent(self, node): """检查是否有已解锁的父节点""" current = node.parent while current: if current.name in self.player_data.get('unlocked_skills', []): return True current = current.parent return False def draw_progress_tree(self): """绘制带进度信息的树状图""" # 计算布局 layout = TreeLayout(node_width=1.8, node_height=0.8, level_spacing=2.5, sibling_spacing=2.0) layout.calculate_positions(self.root) # 创建画布 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 10), gridspec_kw={'width_ratios': [3, 1]}) # 左侧:树状图 ax1.set_aspect('equal') # 获取进度数据 node_heat = self.create_progress_heatmap() # 绘制连接线 self.draw_progress_edges(ax1, self.root) # 绘制节点 self.draw_progress_nodes(ax1, self.root, node_heat) # 设置标题 ax1.set_title("角色技能树进度", fontsize=16, fontweight='bold', pad=20) ax1.axis('off') # 右侧:统计信息 self.draw_statistics_panel(ax2) plt.tight_layout() plt.show() def draw_progress_edges(self, ax, node): """绘制带进度的连接线""" for child in node.children: parent_bottom = (node.x, node.y - node.height/2) child_top = (child.x, child.y + child.height/2) mid_y = (parent_bottom[1] + child_top[1]) / 2 # 根据子节点状态设置线条颜色 child_status = self.get_node_status(child) if child_status == 'unlocked': color = '#00B894' alpha = 0.8 elif child_status == 'available': color = '#FDCB6E' alpha = 0.6 else: color = '#636e72' alpha = 0.3 ax.plot([parent_bottom[0], parent_bottom[0]], [parent_bottom[1], mid_y], color=color, linewidth=2, alpha=alpha) ax.plot([parent_bottom[0], child_top[0]], [mid_y, mid_y], color=color, linewidth=2, alpha=alpha) ax.plot([child_top[0], child_top[0]], [mid_y, child_top[1]], color=color, linewidth=2, alpha=alpha) self.draw_progress_edges(ax, child) def draw_progress_nodes(self, ax, node, node_heat): """绘制带进度的节点""" status = self.get_node_status(node) # 根据状态设置颜色 if status == 'unlocked': facecolor = '#00B894' # 绿色 - 已解锁 text_color = 'white' elif status == 'available': facecolor = '#FDCB6E' # 黄色 - 可解锁 text_color = 'black' else: facecolor = '#636e72' # 灰色 - 未解锁 text_color = 'white' # 绘制节点 rect = patches.Rectangle( (node.x - node.width/2, node.y - node.height/2), node.width, node.height, facecolor=facecolor, edgecolor='black', linewidth=2, alpha=0.9, zorder=10 ) ax.add_patch(rect) # 主文本 ax.text(node.x, node.y, node.name, ha='center', va='center', fontsize=10, fontweight='bold', color=text_color, zorder=11) # 等级文本 if node.value: ax.text(node.x, node.y - node.height/2 - 0.25, f"Lv.{node.value}", ha='center', va='top', fontsize=8, color='gray', zorder=11) # 递归绘制子节点 for child in node.children: self.draw_progress_nodes(ax, child, node_heat) def get_node_status(self, node): """获取节点状态""" if node.name in self.player_data.get('unlocked_skills', []): return 'unlocked' elif self.has_unlocked_parent(node): return 'available' else: return 'locked' def draw_statistics_panel(self, ax): """绘制统计面板""" ax.axis('off') # 分析数据 stats = self.analyze_skill_distribution() # 准备文本 lines = [ "📊 技能树统计分析", "", f"总技能数: {stats['total_skills']}", f"已解锁: {stats['unlocked_skills']}", f"解锁率: {stats['unlocked_skills']/stats['total_skills']*100:.1f}%", "", f"最大深度: {stats['avg_depth']}", f"最受欢迎分支: {stats['most_popular_branch']}", "", "💡 建议:" ] # 生成建议 if stats['unlocked_skills'] / stats['total_skills'] < 0.3: lines.append("- 解锁率较低,建议") lines.append(" 优先解锁核心技能") elif stats['most_popular_branch']: lines.append(f"- {stats['most_popular_branch']}分支") lines.append(" 受欢迎,可考虑") lines.append(" 增加相关内容") # 显示文本 text = "\n".join(lines) ax.text(0, 0.9, text, fontsize=11, va='top', bbox=dict(boxstyle="round,pad=0.5", facecolor="#f0f0f0", edgecolor="black", alpha=0.9)) # 添加进度条 progress = stats['unlocked_skills'] / stats['total_skills'] bar_y = -0.2 bar_width = 0.8 bar_height = 0.1 # 背景条 ax.add_patch(patches.Rectangle((0.1, bar_y), bar_width, bar_height, facecolor='#dfe6e9', edgecolor='black', alpha=0.5)) # 进度条 ax.add_patch(patches.Rectangle((0.1, bar_y), bar_width * progress, bar_height, facecolor='#00b894', edgecolor='black', alpha=0.8)) # 进度文本 ax.text(0.5, bar_y + bar_height/2, f"{progress*100:.1f}%", ha='center', va='center', fontsize=10, fontweight='bold') # 创建示例玩家数据 def create_sample_player_data(): """创建示例玩家数据""" return { 'player_name': '冒险者001', 'level': 25, 'unlocked_skills': [ '战士', '狂战士', '剑圣', '盾卫', '狂暴', '剑气', '神佑' ], 'play_time': 45.5, # 小时 'last_login': datetime.now().strftime('%Y-%m-%d %H:%M') } # 运行分析示例 if __name__ == "__main__": # 创建技能树 skill_tree = create_game_skill_tree() # 创建玩家数据 player_data = create_sample_player_data() # 创建分析器 analyzer = SkillTreeAnalyzer(skill_tree) analyzer.load_player_progress(player_data) # 绘制进度树 analyzer.draw_progress_tree() 这个案例展示了如何将玩家数据与技能树结合,创建可视化的进度展示。关键特性包括: 状态着色 :不同状态的节点使用不同颜色 统计面板 :右侧显示详细的统计信息和建议 进度条 :直观展示整体解锁进度 智能建议 :根据数据生成改进建议 案例2:游戏经济系统树状图 import matplotlib.pyplot as plt import matplotlib.patches as patches import numpy as np def create_economy_tree(): """创建游戏经济系统树""" root = TreeNode("经济系统", "核心", '#FF6B6B') # 货币层级 currency = TreeNode("货币体系", "基础", '#4ECDC4') root.add_child(currency) # 基础货币 copper = TreeNode("铜币", "1:100", '#45B7D1') silver = TreeNode("银币", "1:100", '#45B7D1') gold = TreeNode("金币", "基础", '#45B7D1') currency.add_child(copper) currency.add_child(silver) currency.add_child(gold) # 高级货币 premium = TreeNode("高级货币", "付费", '#96CEB4') root.add_child(premium) diamond = TreeNode("钻石", "1:10", '#FFEAA7') crystal = TreeNode("水晶", "稀有", '#FFEAA7') premium.add_child(diamond) premium.add_child(crystal) # 交易系统 trading = TreeNode("交易系统", "市场", '#4ECDC4') root.add_child(trading) auction = TreeNode("拍卖行", "玩家间", '#45B7D1') shop = TreeNode("商店", "NPC", '#45B7D1') trading.add_child(auction) trading.add_child(shop) # 商店子类 weapon_shop = TreeNode("武器店", "装备", '#FFEAA7') item_shop = TreeNode("道具店", "消耗品", '#FFEAA7') shop.add_child(weapon_shop) shop.add_child(item_shop) return root def draw_economy_tree_with_flow(root): """绘制带资源流向的经济树""" # 计算布局 layout = TreeLayout(node_width=2.0, node_height=0.8, level_spacing=2.5, sibling_spacing=2.0) layout.calculate_positions(root) # 创建画布 fig, ax = plt.subplots(figsize=(16, 10)) ax.set_aspect('equal') # 绘制连接线(带箭头表示流向) def draw_flow_edges(node): for child in node.children: parent_bottom = (node.x, node.y - node.height/2) child_top = (child.x, child.y + child.height/2) mid_y = (parent_bottom[1] + child_top[1]) / 2 # 绘制线条 ax.plot([parent_bottom[0], parent_bottom[0]], [parent_bottom[1], mid_y], color='#2d3436', linewidth=2, alpha=0.6) ax.plot([parent_bottom[0], child_top[0]], [mid_y, mid_y], color='#2d3436', linewidth=2, alpha=0.6) ax.plot([child_top[0], child_top[0]], [mid_y, child_top[1]], color='#2d3436', linewidth=2, alpha=0.6) # 添加箭头表示流向 arrow = patches.FancyArrowPatch( (child_top[0], child_top[1] + 0.1), (child_top[0], child_top[1] + 0.05), arrowstyle='->‘,
mutation_scale=15,
color=’#0984e3′,
alpha=0.7
)
ax.add_patch(arrow)

draw_flow_edges(child)

draw_flow_edges(root)

# 绘制节点
def draw_economy_nodes(node):
# 创建圆角矩形
rect = patches.FancyBboxPatch(
(node.x – node.width/2, node.y – node.height/2),
node.width, node.height,
boxstyle=”round,pad=0.1″,
facecolor=node.color,
edgecolor=’black’,
linewidth=2,
alpha=0.9,
zorder=10
)
ax.add_patch(rect)

# 主文本
ax.text(node.x, node.y, node.name,
ha=’center’, va=’center’,
fontsize=10, fontweight=’bold’,
color=’white’ if node.color not in [‘#FFEAA7’, ‘#FDCB6E’] else ‘black’,
zorder=11)

# 子文本(汇率/类型)
if node.value:
ax.text(node.x, node.y – node.height/2 – 0.25,
node.value,
ha=’center’, va=’top’,
fontsize=8, color=’#2d3436′, zorder=11)

for child in node.children:
draw_economy_nodes(child)

draw_economy_nodes(root)

# 设置范围
all_nodes = []
def collect(node):
all_nodes.append(node)
for child in node.children:
collect(child)
collect(root)

x_coords = [n.x for n in all_nodes]
y_coords = [n.y for n in all_nodes]

ax.set_xlim(min(x_coords) – 3, max(x_coords) + 3)
ax.set_ylim(min(y_coords) – 2, max(y_coords) + 2)

# 添加图例
legend_elements = [
patches.Patch(facecolor=’#FF6B6B’, label=’核心系统’),
patches.Patch(facecolor=’#4ECDC4′, label=’主要模块’),
patches.Patch(facecolor=’#45B7D1′, label=’子模块’),
patches.Patch(facecolor=’#FFEAA7′, label=’具体项目’),
patches.FancyArrowPatch((0, 0), (0.5, 0), arrowstyle=’->’, color=’#0984e3′, label=’资源流向’)
]
ax.legend(handles=legend_elements, loc=’upper right’, bbox_to_anchor=(1.15, 1.0))

ax.set_title(“游戏经济系统树状图”, fontsize=18, fontweight=’bold’, pad=20)
ax.axis(‘off’)

plt.tight_layout()
plt.show()

# 运行经济系统示例
if __name__ == “__main__”:
economy_tree = create_economy_tree()
draw_economy_tree_with_flow(economy_tree)
这个案例展示了游戏经济系统的层次结构,使用了圆角矩形和箭头来增强视觉效果,清晰地展示了货币层级和交易流向。
优化与美化技巧
1. 颜色方案优化
# 定义专业的游戏主题颜色方案
GAME_COLOR_THEMES = {
‘rpg’: {
‘root’: ‘#C44569’,
‘branch’: ‘#FFA502’,
‘leaf’: ‘#00B894’,
‘background’: ‘#F0F3F8’
},
‘strategy’: {
‘root’: ‘#2C3E50’,
‘branch’: ‘#3498DB’,
‘leaf’: ‘#27AE60’,
‘background’: ‘#ECF0F1’
},
‘fantasy’: {
‘root’: ‘#8E44AD’,
‘branch’: ‘#3498DB’,
‘leaf’: ‘#1ABC9C’,
‘background’: ‘#FDF2E9’
},
‘sci-fi’: {
‘root’: ‘#E74C3C’,
‘branch’: ‘#9B59B6’,
‘leaf’: ‘#3498DB’,
‘background’: ‘#2C3E50’
}
}

def apply_color_theme(theme_name, node):
“””应用颜色主题”””
if theme_name not in GAME_COLOR_THEMES:
theme_name = ‘rpg’

theme = GAME_COLOR_THEMES[theme_name]

# 根据深度分配颜色
def get_depth(node):
depth = 0
current = node
while current.parent:
depth += 1
current = current.parent
return depth

depth = get_depth(node)

if depth == 0:
node.color = theme[‘root’]
elif depth == 1:
node.color = theme[‘branch’]
else:
node.color = theme[‘leaf’]

for child in node.children:
apply_color_theme(theme_name, child)
2. 动画效果
import matplotlib.animation as animation

def animate_tree_growth(root_node, frames=30):
“””创建树生长动画”””
fig, ax = plt.subplots(figsize=(12, 8))
ax.set_aspect(‘equal’)

# 计算布局
layout = TreeLayout(node_width=1.2, node_height=0.6, level_spacing=2.5, sibling_spacing=1.8)
layout.calculate_positions(root_node)

# 收集所有节点
all_nodes = []
def collect(node):
all_nodes.append(node)
for child in node.children:
collect(child)
collect(root_node)

# 按深度排序
all_nodes.sort(key=lambda n: n.y, reverse=True)

# 初始化绘图元素
patches_list = []
texts_list = []

def init():
return []

def update(frame):
# 清除当前帧
for p in patches_list:
p.remove()
for t in texts_list:
t.remove()
patches_list.clear()
texts_list.clear()

# 计算当前帧应显示的节点数
nodes_to_show = int(len(all_nodes) * (frame / frames))

# 绘制连接线(只绘制已显示的节点之间的连接)
drawn_nodes = all_nodes[:nodes_to_show]
drawn_set = set(drawn_nodes)

for node in drawn_nodes:
for child in node.children:
if child in drawn_set:
parent_bottom = (node.x, node.y – node.height/2)
child_top = (child.x, child.y + child.height/2)
mid_y = (parent_bottom[1] + child_top[1]) / 2

line1, = ax.plot([parent_bottom[0], parent_bottom[0]], [parent_bottom[1], mid_y],
color=’gray’, linewidth=2, alpha=0.6)
line2, = ax.plot([parent_bottom[0], child_top[0]], [mid_y, mid_y],
color=’gray’, linewidth=2, alpha=0.6)
line3, = ax.plot([child_top[0], child_top[0]], [mid_y, child_top[1]],
color=’gray’, linewidth=2, alpha=0.6)
patches_list.extend([line1, line2, line3])

# 绘制节点
for node in drawn_nodes:
rect = patches.Rectangle(
(node.x – node.width/2, node.y – node.height/2),
node.width, node.height,
facecolor=node.color,
edgecolor=’black’,
linewidth=2,
alpha=0.9
)
ax.add_patch(rect)
patches_list.append(rect)

text = ax.text(node.x, node.y, node.name,
ha=’center’, va=’center’,
fontsize=10, fontweight=’bold’,
color=’white’)
texts_list.append(text)

# 设置范围
if drawn_nodes:
x_coords = [n.x for n in drawn_nodes]
y_coords = [n.y for n in drawn_nodes]
ax.set_xlim(min(x_coords) – 2, max(x_coords) + 2)
ax.set_ylim(min(y_coords) – 2, max(y_coords) + 2)

ax.set_title(f”技能树生长动画 – 帧 {frame}/{frames}”, fontsize=16, fontweight=’bold’)
ax.axis(‘off’)

return patches_list + texts_list

# 创建动画
anim = animation.FuncAnimation(fig, update, frames=frames + 1, init_func=init,
interval=200, blit=False, repeat=False)

return anim

# 使用示例
# anim = animate_tree_growth(skill_tree)
# anim.save(‘skill_tree_growth.gif’, writer=’pillow’, fps=5)
3. 导出与分享
def export_tree_data(root_node, filename):
“””导出树结构为JSON格式”””
import json

def node_to_dict(node):
return {
‘name’: node.name,
‘value’: node.value,
‘color’: node.color,
‘children’: [node_to_dict(child) for child in node.children]
}

tree_data = node_to_dict(root_node)

with open(filename, ‘w’, encoding=’utf-8′) as f:
json.dump(tree_data, f, ensure_ascii=False, indent=2)

print(f”树结构已导出到 {filename}”)

def import_tree_data(filename):
“””从JSON导入树结构”””
import json

with open(filename, ‘r’, encoding=’utf-8′) as f:
data = json.load(f)

def dict_to_node(d):
node = TreeNode(d[‘name’], d.get(‘value’), d.get(‘color’, ‘#6C5CE7’))
for child_data in d.get(‘children’, []):
node.add_child(dict_to_node(child_data))
return node

return dict_to_node(data)

# 使用示例
# export_tree_data(skill_tree, ‘game_skill_tree.json’)
# loaded_tree = import_tree_data(‘game_skill_tree.json’)
性能优化与最佳实践
1. 处理大型树结构
当树结构非常庞大时(例如包含数百个节点),需要考虑性能优化:
import time

class OptimizedTreePlotter:
“””优化的树状图绘制器”””

def __init__(self, root_node, max_nodes=100):
self.root = root_node
self.max_nodes = max_nodes
self.node_count = 0

def count_nodes(self, node):
“””计算节点数量”””
count = 1
for child in node.children:
count += self.count_nodes(child)
return count

def should_collapse_by_default(self, node):
“””判断是否默认折叠”””
total_nodes = self.count_nodes(node)
return total_nodes > self.max_nodes

def draw_optimized_tree(self):
“””绘制优化的树状图”””
total_nodes = self.count_nodes(self.root)

if total_nodes > self.max_nodes:
print(f”警告:树结构包含 {total_nodes} 个节点,超过阈值 {self.max_nodes}”)
print(“将默认折叠部分节点以提高性能”)

# 自动折叠深层节点
self.auto_collapse_deep_nodes(self.root, depth=0)

# 使用之前的交互式绘图器
plotter = InteractiveTreePlotter(self.root)
return plotter

def auto_collapse_deep_nodes(self, node, depth=0, max_depth=2):
“””自动折叠深层节点”””
if depth > max_depth and node.children:
self.root.collapsed_nodes.add(node)

for child in node.children:
self.auto_collapse_deep_nodes(child, depth + 1, max_depth)

# 使用示例
# large_tree = create_large_tree() # 假设创建了一个大树
# plotter = OptimizedTreePlotter(large_tree, max_nodes=50)
# plotter.draw_optimized_tree()
2. 内存管理
def clear_tree_memory(tree_plotter):
“””清理树状图占用的内存”””
if hasattr(tree_plotter, ‘fig’):
plt.close(tree_plotter.fig)
if hasattr(tree_plotter, ‘node_patches’):
tree_plotter.node_patches.clear()
if hasattr(tree_plotter, ‘node_texts’):
tree_plotter.node_texts.clear()
if hasattr(tree_plotter, ‘node_map’):
tree_plotter.node_map.clear()
3. 代码组织最佳实践
# 推荐的项目结构
“””
game_tree_visualization/
├── core/
│ ├── __init__.py
│ ├── tree_nodes.py # TreeNode类定义
│ ├── tree_layout.py # 布局算法
│ └── tree_drawer.py # 绘图逻辑
├── utils/
│ ├── color_themes.py # 颜色方案
│ ├── data_import.py # 数据导入
│ └── export.py # 导出功能
├── examples/
│ ├── skill_tree.py # 技能树示例
│ ├── economy_tree.py # 经济系统示例
│ └── analysis.py # 分析工具
└── main.py # 主程序
“””
总结
本文详细介绍了如何使用Matplotlib绘制游戏内的树状图,从基础概念到高级实现,涵盖了以下关键内容:
基础概念
:理解树状图的结构和游戏应用场景
手动绘制
:基础的节点和连接线绘制方法
自动布局
:实现自动计算节点位置的算法
交互功能
:添加点击、展开/折叠等交互特性
实战案例
:技能树分析和经济系统可视化
优化技巧
:颜色方案、动画效果、性能优化
通过这些技术,你可以创建专业、美观且功能丰富的游戏数据可视化工具,帮助团队更好地理解和优化游戏设计。无论是用于开发调试、玩家数据分析,还是作为游戏内的UI元素,这些树状图都能发挥重要作用。
记住,好的可视化不仅要美观,更要能清晰地传达信息。在实际应用中,始终考虑用户的需求和使用场景,选择合适的可视化方式。