调试实体和组件属性

This commit is contained in:
YHH
2025-10-16 11:55:41 +08:00
parent fcf3def284
commit c876edca0c
7 changed files with 525 additions and 50 deletions

View File

@@ -206,7 +206,8 @@ export class DebugManager implements IService, IUpdatable {
return;
}
const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath);
const scene = this.sceneManager.currentScene;
const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath, scene);
this.webSocketManager.send({
type: 'expand_lazy_object_response',
@@ -238,7 +239,8 @@ export class DebugManager implements IService, IUpdatable {
return;
}
const properties = this.entityCollector.getComponentProperties(entityId, componentIndex);
const scene = this.sceneManager.currentScene;
const properties = this.entityCollector.getComponentProperties(entityId, componentIndex, scene);
this.webSocketManager.send({
type: 'get_component_properties_response',
@@ -261,7 +263,8 @@ export class DebugManager implements IService, IUpdatable {
try {
const { requestId } = message;
const rawEntityList = this.entityCollector.getRawEntityList();
const scene = this.sceneManager.currentScene;
const rawEntityList = this.entityCollector.getRawEntityList(scene);
this.webSocketManager.send({
type: 'get_raw_entity_list_response',
@@ -293,7 +296,8 @@ export class DebugManager implements IService, IUpdatable {
return;
}
const entityDetails = this.entityCollector.getEntityDetails(entityId);
const scene = this.sceneManager.currentScene;
const entityDetails = this.entityCollector.getEntityDetails(entityId, scene);
this.webSocketManager.send({
type: 'get_entity_details_response',

View File

@@ -263,8 +263,7 @@ export class EntityDataCollector {
componentCount: entity.components?.length || 0,
memory: 0
}))
.sort((a: any, b: any) => b.componentCount - a.componentCount)
.slice(0, 10);
.sort((a: any, b: any) => b.componentCount - a.componentCount);
}
@@ -303,7 +302,7 @@ export class EntityDataCollector {
});
if (archetype.entities) {
archetype.entities.slice(0, 5).forEach((entity: any) => {
archetype.entities.forEach((entity: any) => {
topEntities.push({
id: entity.id.toString(),
name: entity.name || `Entity_${entity.id}`,
@@ -352,7 +351,7 @@ export class EntityDataCollector {
});
if (archetype.entities) {
archetype.entities.slice(0, 5).forEach((entity: any) => {
archetype.entities.forEach((entity: any) => {
topEntities.push({
id: entity.id.toString(),
name: entity.name || `Entity_${entity.id}`,

View File

@@ -131,10 +131,10 @@ async fn handle_connection(
while let Some(msg) = ws_receiver.next().await {
match msg {
Ok(Message::Text(text)) => {
// Parse incoming debug data from game client
// Parse incoming messages
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&text) {
if json_value.get("type").and_then(|t| t.as_str()) == Some("debug_data") {
// Broadcast to frontend (ProfilerWindow)
// Broadcast debug data from game client to all clients (including frontend)
tx.send(text).ok();
} else if json_value.get("type").and_then(|t| t.as_str()) == Some("ping") {
// Respond to ping
@@ -145,6 +145,10 @@ async fn handle_connection(
})
.to_string(),
);
} else {
// Forward all other messages (like get_raw_entity_list, get_entity_details, etc.)
// to all connected clients (this enables frontend -> game client communication)
tx.send(text).ok();
}
}
}

View File

@@ -13,19 +13,42 @@ interface EntityInspectorProps {
export function EntityInspector({ entityStore: _entityStore, messageHub }: EntityInspectorProps) {
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
const [remoteEntity, setRemoteEntity] = useState<any | null>(null);
const [remoteEntityDetails, setRemoteEntityDetails] = useState<any | null>(null);
const [showAddComponent, setShowAddComponent] = useState(false);
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
useEffect(() => {
const handleSelection = (data: { entity: Entity | null }) => {
setSelectedEntity(data.entity);
setRemoteEntity(null);
setRemoteEntityDetails(null);
setShowAddComponent(false);
};
const handleRemoteSelection = (data: { entity: any }) => {
setRemoteEntity(data.entity);
setRemoteEntityDetails(null);
setSelectedEntity(null);
setShowAddComponent(false);
};
const handleEntityDetails = (event: Event) => {
const customEvent = event as CustomEvent;
const details = customEvent.detail;
console.log('[EntityInspector] Received entity details:', details);
setRemoteEntityDetails(details);
};
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteSelection);
window.addEventListener('profiler:entity-details', handleEntityDetails);
return () => {
unsubSelect();
unsubRemoteSelect();
window.removeEventListener('profiler:entity-details', handleEntityDetails);
};
}, [messageHub]);
@@ -82,7 +105,215 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
});
};
if (!selectedEntity) {
const renderRemoteProperty = (key: string, value: any) => {
if (value === null || value === undefined) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text">null</span>
</div>
);
}
if (Array.isArray(value)) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div style={{ flex: 1, display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{value.length === 0 ? (
<span className="property-value-text" style={{ opacity: 0.5 }}>Empty Array</span>
) : (
value.map((item, index) => (
<span
key={index}
style={{
padding: '2px 6px',
background: 'var(--color-bg-inset)',
border: '1px solid var(--color-border-default)',
borderRadius: '3px',
fontSize: '10px',
color: 'var(--color-text-primary)',
fontFamily: 'var(--font-family-mono)'
}}
>
{typeof item === 'object' ? JSON.stringify(item) : String(item)}
</span>
))
)}
</div>
</div>
);
}
const valueType = typeof value;
if (valueType === 'boolean') {
return (
<div key={key} className="property-field property-field-boolean">
<label className="property-label">{key}</label>
<div className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'} property-toggle-readonly`}>
<span className="property-toggle-thumb" />
</div>
</div>
);
}
if (valueType === 'number') {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<input
type="number"
className="property-input property-input-number"
value={value}
disabled
/>
</div>
);
}
if (valueType === 'string') {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<input
type="text"
className="property-input property-input-text"
value={value}
disabled
/>
</div>
);
}
if (valueType === 'object' && value.minX !== undefined && value.maxX !== undefined && value.minY !== undefined && value.maxY !== undefined) {
return (
<div key={key} className="property-field" style={{ flexDirection: 'column', alignItems: 'stretch' }}>
<label className="property-label" style={{ flex: 'none', marginBottom: '4px' }}>{key}</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.minX}
disabled
placeholder="Min"
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.maxX}
disabled
placeholder="Max"
/>
</div>
</div>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.minY}
disabled
placeholder="Min"
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.maxY}
disabled
placeholder="Max"
/>
</div>
</div>
</div>
</div>
);
}
if (valueType === 'object' && value.x !== undefined && value.y !== undefined) {
if (value.z !== undefined) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.x}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.y}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-z">Z</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.z}
disabled
/>
</div>
</div>
</div>
);
} else {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.x}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.y}
disabled
/>
</div>
</div>
</div>
);
}
}
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text">{JSON.stringify(value)}</span>
</div>
);
};
if (!selectedEntity && !remoteEntity) {
return (
<div className="entity-inspector">
<div className="inspector-header">
@@ -100,7 +331,102 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
);
}
const components = selectedEntity.components;
// 显示远程实体
if (remoteEntity) {
const displayData = remoteEntityDetails || remoteEntity;
const hasDetailedComponents = remoteEntityDetails && remoteEntityDetails.components && remoteEntityDetails.components.length > 0;
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content scrollable">
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Entity Info (Remote)</span>
</div>
<div className="section-content">
<div className="info-row">
<span className="info-label">ID:</span>
<span className="info-value">{displayData.id}</span>
</div>
<div className="info-row">
<span className="info-label">Name:</span>
<span className="info-value">{displayData.name}</span>
</div>
<div className="info-row">
<span className="info-label">Enabled:</span>
<span className="info-value">{displayData.enabled ? 'Yes' : 'No'}</span>
</div>
{displayData.scene && (
<div className="info-row">
<span className="info-label">Scene:</span>
<span className="info-value">{displayData.scene}</span>
</div>
)}
</div>
</div>
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Components ({displayData.componentCount})</span>
</div>
<div className="section-content">
{hasDetailedComponents ? (
<ul className="component-list">
{remoteEntityDetails!.components.map((component: any, index: number) => {
const isExpanded = expandedComponents.has(index);
return (
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
<button
className="component-expand-btn"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<Settings size={14} className="component-icon" />
<span className="component-name">{component.typeName}</span>
</div>
{isExpanded && (
<div className="component-properties animate-slideDown">
<div className="property-inspector">
{Object.entries(component.properties).map(([key, value]) =>
renderRemoteProperty(key, value)
)}
</div>
</div>
)}
</li>
);
})}
</ul>
) : displayData.componentTypes && displayData.componentTypes.length > 0 ? (
<ul className="component-list">
{displayData.componentTypes.map((componentType: string, index: number) => (
<li key={index} className="component-item">
<div className="component-header">
<Settings size={14} className="component-icon" />
<span className="component-name">{componentType}</span>
</div>
</li>
))}
</ul>
) : (
<div className="empty-state-small">No components</div>
)}
</div>
</div>
</div>
</div>
);
}
const components = selectedEntity!.components;
return (
<div className="entity-inspector">
@@ -121,11 +447,11 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
</div>
<div className="info-row">
<span className="info-label">Name:</span>
<span className="info-value">Entity {selectedEntity.id}</span>
<span className="info-value">Entity {selectedEntity!.id}</span>
</div>
<div className="info-row">
<span className="info-label">Enabled:</span>
<span className="info-value">{selectedEntity.enabled ? 'Yes' : 'No'}</span>
<span className="info-value">{selectedEntity!.enabled ? 'Yes' : 'No'}</span>
</div>
</div>
</div>

View File

@@ -74,6 +74,27 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
entityStore.selectEntity(entity);
};
const handleRemoteEntityClick = (entity: RemoteEntity) => {
setSelectedId(entity.id);
// 请求完整的实体详情(包含组件属性)
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
if (profilerService) {
profilerService.requestEntityDetails(entity.id);
}
// 先发布基本信息,详细信息稍后通过 ProfilerService 异步返回
messageHub.publish('remote-entity:selected', {
entity: {
id: entity.id,
name: entity.name,
enabled: entity.enabled,
componentCount: entity.componentCount,
componentTypes: entity.componentTypes
}
});
};
// Determine which entities to display
const displayEntities = isRemoteConnected ? remoteEntities : entities;
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
@@ -105,13 +126,14 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
{remoteEntities.map(entity => (
<li
key={entity.id}
className={`entity-item remote-entity ${!entity.enabled ? 'disabled' : ''}`}
title={`${entity.name} - ${entity.components.join(', ')}`}
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`}
title={`${entity.name} - ${entity.componentTypes.join(', ')}`}
onClick={() => handleRemoteEntityClick(entity)}
>
<Box size={14} className="entity-icon" />
<span className="entity-name">{entity.name}</span>
{entity.components.length > 0 && (
<span className="component-count">{entity.components.length}</span>
{entity.componentCount > 0 && (
<span className="component-count">{entity.componentCount}</span>
)}
</li>
))}

View File

@@ -12,7 +12,35 @@ export interface RemoteEntity {
id: number;
name: string;
enabled: boolean;
components: string[];
active: boolean;
activeInHierarchy: boolean;
componentCount: number;
componentTypes: string[];
parentId: number | null;
childIds: number[];
depth: number;
tag: number;
updateOrder: number;
}
export interface RemoteComponentDetail {
typeName: string;
properties: Record<string, any>;
}
export interface RemoteEntityDetails {
id: number;
name: string;
enabled: boolean;
active: boolean;
activeInHierarchy: boolean;
scene: string;
sceneName: string;
sceneType: string;
componentCount: number;
componentTypes: string[];
components: RemoteComponentDetail[];
parentName: string | null;
}
export interface ProfilerData {
@@ -140,6 +168,10 @@ export class ProfilerService {
const message = JSON.parse(event.data);
if (message.type === 'debug_data' && message.data) {
this.handleDebugData(message.data);
} else if (message.type === 'get_raw_entity_list_response' && message.data) {
this.handleRawEntityListResponse(message.data);
} else if (message.type === 'get_entity_details_response' && message.data) {
this.handleEntityDetailsResponse(message.data);
}
} catch (error) {
console.error('[ProfilerService] Failed to parse message:', error);
@@ -152,6 +184,25 @@ export class ProfilerService {
}
}
public requestEntityDetails(entityId: number): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.warn('[ProfilerService] Cannot request entity details: WebSocket not connected');
return;
}
try {
const request = {
type: 'get_entity_details',
requestId: `entity_details_${entityId}_${Date.now()}`,
entityId
};
console.log('[ProfilerService] Requesting entity details:', request);
this.ws.send(JSON.stringify(request));
} catch (error) {
console.error('[ProfilerService] Failed to request entity details:', error);
}
}
private handleDebugData(debugData: any): void {
const performance = debugData.performance;
if (!performance) return;
@@ -179,49 +230,98 @@ export class ProfilerService {
});
}
const entityCount = debugData.entities?.totalCount || 0;
const entityCount = debugData.entities?.totalEntities || debugData.entities?.totalCount || 0;
const componentTypes = debugData.components?.types || [];
const componentCount = componentTypes.length;
// 解析实体列表
console.log('[ProfilerService] debugData.entities:', debugData.entities);
let entities: RemoteEntity[] = [];
// 尝试从 topEntitiesByComponents 获取实体列表
if (debugData.entities?.topEntitiesByComponents && Array.isArray(debugData.entities.topEntitiesByComponents)) {
console.log('[ProfilerService] Found topEntitiesByComponents, length:', debugData.entities.topEntitiesByComponents.length);
entities = debugData.entities.topEntitiesByComponents.map((e: any) => ({
id: parseInt(e.id) || 0,
name: e.name || `Entity ${e.id}`,
enabled: true, // topEntitiesByComponents doesn't have enabled flag, assume true
components: [] // componentCount is provided but not component names
}));
console.log('[ProfilerService] Parsed entities from topEntitiesByComponents:', entities.length);
}
// 尝试从 entities 获取实体列表(旧格式兼容)
else if (debugData.entities?.entities && Array.isArray(debugData.entities.entities)) {
console.log('[ProfilerService] Found entities array, length:', debugData.entities.entities.length);
entities = debugData.entities.entities.map((e: any) => ({
id: e.id,
name: e.name || `Entity ${e.id}`,
enabled: e.enabled !== false,
components: e.components || []
}));
console.log('[ProfilerService] Parsed entities:', entities.length);
} else {
console.log('[ProfilerService] No entities array found');
}
this.currentData = {
totalFrameTime,
systems,
entityCount,
componentCount,
fps,
entities
entities: []
};
this.notifyListeners(this.currentData);
// 请求完整的实体列表
this.requestRawEntityList();
}
private requestRawEntityList(): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.warn('[ProfilerService] Cannot request entity list: WebSocket not connected');
return;
}
try {
const request = {
type: 'get_raw_entity_list',
requestId: `entity_list_${Date.now()}`
};
console.log('[ProfilerService] Requesting entity list:', request);
this.ws.send(JSON.stringify(request));
} catch (error) {
console.error('[ProfilerService] Failed to request entity list:', error);
}
}
private handleRawEntityListResponse(data: any): void {
if (!data || !Array.isArray(data)) {
console.warn('[ProfilerService] Invalid raw entity list response:', data);
return;
}
console.log('[ProfilerService] Received raw entity list, count:', data.length);
const entities: RemoteEntity[] = data.map((e: any) => ({
id: e.id,
name: e.name || `Entity ${e.id}`,
enabled: e.enabled !== false,
active: e.active !== false,
activeInHierarchy: e.activeInHierarchy !== false,
componentCount: e.componentCount || 0,
componentTypes: e.componentTypes || [],
parentId: e.parentId || null,
childIds: e.childIds || [],
depth: e.depth || 0,
tag: e.tag || 0,
updateOrder: e.updateOrder || 0
}));
if (this.currentData) {
this.currentData.entities = entities;
this.notifyListeners(this.currentData);
}
}
private handleEntityDetailsResponse(data: any): void {
if (!data) {
console.warn('[ProfilerService] Invalid entity details response:', data);
return;
}
console.log('[ProfilerService] Received entity details:', data);
const entityDetails: RemoteEntityDetails = {
id: data.id,
name: data.name || `Entity ${data.id}`,
enabled: data.enabled !== false,
active: data.active !== false,
activeInHierarchy: data.activeInHierarchy !== false,
scene: data.scene || '',
sceneName: data.sceneName || '',
sceneType: data.sceneType || '',
componentCount: data.componentCount || 0,
componentTypes: data.componentTypes || [],
components: data.components || [],
parentName: data.parentName || null
};
window.dispatchEvent(new CustomEvent('profiler:entity-details', {
detail: entityDetails
}));
}
private createEmptyData(): ProfilerData {

View File

@@ -39,6 +39,26 @@
min-height: 0;
}
.inspector-content::-webkit-scrollbar {
width: 14px;
}
.inspector-content::-webkit-scrollbar-track {
background: transparent;
}
.inspector-content::-webkit-scrollbar-thumb {
background: rgba(121, 121, 121, 0.4);
border-radius: 8px;
border: 3px solid transparent;
background-clip: padding-box;
}
.inspector-content::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.7);
background-clip: padding-box;
}
.inspector-section {
margin-bottom: var(--spacing-lg);
}