feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 (#244)
* feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 * fix: 修复 CI 流程并清理代码
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"@esengine/behavior-tree": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@esengine/ecs-components": "workspace:*",
|
||||
"@esengine/physics-rapier2d": "workspace:*",
|
||||
"@esengine/tilemap": "workspace:*",
|
||||
"@esengine/ui": "workspace:*",
|
||||
"@esengine/ecs-engine-bindgen": "workspace:*",
|
||||
|
||||
@@ -795,18 +795,27 @@ function App() {
|
||||
const dynamicPanels: FlexDockPanel[] = activeDynamicPanels
|
||||
.filter((panelId) => {
|
||||
const panelDesc = uiRegistry.getPanel(panelId);
|
||||
return panelDesc && panelDesc.component;
|
||||
return panelDesc && (panelDesc.component || panelDesc.render);
|
||||
})
|
||||
.map((panelId) => {
|
||||
const panelDesc = uiRegistry.getPanel(panelId)!;
|
||||
const Component = panelDesc.component;
|
||||
// 优先使用动态标题,否则使用默认标题
|
||||
const customTitle = dynamicPanelTitles.get(panelId);
|
||||
const defaultTitle = (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title;
|
||||
|
||||
// 支持 component 或 render 两种方式
|
||||
let content: React.ReactNode;
|
||||
if (panelDesc.component) {
|
||||
const Component = panelDesc.component;
|
||||
content = <Component projectPath={currentProjectPath} />;
|
||||
} else if (panelDesc.render) {
|
||||
content = panelDesc.render();
|
||||
}
|
||||
|
||||
return {
|
||||
id: panelDesc.id,
|
||||
title: customTitle || defaultTitle,
|
||||
content: <Component projectPath={currentProjectPath} />,
|
||||
content,
|
||||
closable: panelDesc.closable ?? true
|
||||
};
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ProjectSettingsPlugin } from '../../plugins/builtin/ProjectSettingsPlug
|
||||
import { TilemapPlugin } from '@esengine/tilemap';
|
||||
import { UIPlugin } from '@esengine/ui';
|
||||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||
import { Physics2DPlugin } from '@esengine/physics-rapier2d';
|
||||
|
||||
export class PluginInstaller {
|
||||
/**
|
||||
@@ -50,6 +51,7 @@ export class PluginInstaller {
|
||||
{ name: 'TilemapPlugin', plugin: TilemapPlugin },
|
||||
{ name: 'UIPlugin', plugin: UIPlugin },
|
||||
{ name: 'BehaviorTreePlugin', plugin: BehaviorTreePlugin },
|
||||
{ name: 'Physics2DPlugin', plugin: Physics2DPlugin },
|
||||
];
|
||||
|
||||
for (const { name, plugin } of modulePlugins) {
|
||||
|
||||
@@ -34,9 +34,6 @@ import {
|
||||
SpriteAnimatorComponent,
|
||||
TextComponent,
|
||||
CameraComponent,
|
||||
RigidBodyComponent,
|
||||
BoxColliderComponent,
|
||||
CircleColliderComponent,
|
||||
AudioSourceComponent
|
||||
} from '@esengine/ecs-components';
|
||||
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||||
@@ -114,9 +111,6 @@ export class ServiceRegistry {
|
||||
{ name: 'SpriteAnimatorComponent', type: SpriteAnimatorComponent, editorName: 'SpriteAnimator', category: 'components.category.rendering', description: 'components.spriteAnimator.description', icon: 'Film' },
|
||||
{ name: 'TextComponent', type: TextComponent, editorName: 'Text', category: 'components.category.rendering', description: 'components.text.description', icon: 'Type' },
|
||||
{ name: 'CameraComponent', type: CameraComponent, editorName: 'Camera', category: 'components.category.rendering', description: 'components.camera.description', icon: 'Camera' },
|
||||
{ name: 'RigidBodyComponent', type: RigidBodyComponent, editorName: 'RigidBody', category: 'components.category.physics', description: 'components.rigidBody.description', icon: 'Atom' },
|
||||
{ name: 'BoxColliderComponent', type: BoxColliderComponent, editorName: 'BoxCollider', category: 'components.category.physics', description: 'components.boxCollider.description', icon: 'Square' },
|
||||
{ name: 'CircleColliderComponent', type: CircleColliderComponent, editorName: 'CircleCollider', category: 'components.category.physics', description: 'components.circleCollider.description', icon: 'Circle' },
|
||||
{ name: 'AudioSourceComponent', type: AudioSourceComponent, editorName: 'AudioSource', category: 'components.category.audio', description: 'components.audioSource.description', icon: 'Volume2' },
|
||||
{ name: 'BehaviorTreeRuntimeComponent', type: BehaviorTreeRuntimeComponent, editorName: 'BehaviorTreeRuntime', category: 'components.category.ai', description: 'components.behaviorTreeRuntime.description', icon: 'GitBranch' }
|
||||
];
|
||||
|
||||
@@ -341,31 +341,37 @@ export function EntityInspector({ entity, messageHub, commandManager, componentV
|
||||
/>
|
||||
}
|
||||
{/* Dynamic component actions from plugins */}
|
||||
{componentActionRegistry?.getActionsForComponent(componentName).map((action) => (
|
||||
<button
|
||||
key={action.id}
|
||||
className="component-action-btn"
|
||||
onClick={() => action.execute(component, entity)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '8px 12px',
|
||||
width: '100%',
|
||||
marginTop: '8px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--accent-color, #0078d4)',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
{componentActionRegistry?.getActionsForComponent(componentName).map((action) => {
|
||||
// 解析图标:支持字符串(Lucide 图标名)或 React 元素
|
||||
const ActionIcon = typeof action.icon === 'string'
|
||||
? (LucideIcons as unknown as Record<string, React.ComponentType<{ size?: number }>>)[action.icon]
|
||||
: null;
|
||||
return (
|
||||
<button
|
||||
key={action.id}
|
||||
className="component-action-btn"
|
||||
onClick={() => action.execute(component, entity)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '8px 12px',
|
||||
width: '100%',
|
||||
marginTop: '8px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--accent-color, #0078d4)',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{ActionIcon ? <ActionIcon size={14} /> : action.icon}
|
||||
{action.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, SpriteAni
|
||||
import { TilemapComponent, TilemapRenderingSystem } from '@esengine/tilemap';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
import { UIRenderDataProvider, invalidateUIRenderCaches, UIInputSystem } from '@esengine/ui';
|
||||
import { Physics2DSystem } from '@esengine/physics-rapier2d';
|
||||
import * as esEngine from '@esengine/engine';
|
||||
import {
|
||||
AssetManager,
|
||||
@@ -36,6 +37,7 @@ export class EngineService {
|
||||
private animatorSystem: SpriteAnimatorSystem | null = null;
|
||||
private tilemapSystem: TilemapRenderingSystem | null = null;
|
||||
private behaviorTreeSystem: BehaviorTreeExecutionSystem | null = null;
|
||||
private physicsSystem: Physics2DSystem | null = null;
|
||||
private uiRenderProvider: UIRenderDataProvider | null = null;
|
||||
private uiInputSystem: UIInputSystem | null = null;
|
||||
private initialized = false;
|
||||
@@ -244,6 +246,7 @@ export class EngineService {
|
||||
this.animatorSystem = context.animatorSystem as SpriteAnimatorSystem | undefined ?? null;
|
||||
this.tilemapSystem = context.tilemapSystem as TilemapRenderingSystem | undefined ?? null;
|
||||
this.behaviorTreeSystem = context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined ?? null;
|
||||
this.physicsSystem = context.physicsSystem as Physics2DSystem | undefined ?? null;
|
||||
this.uiRenderProvider = context.uiRenderProvider as UIRenderDataProvider | undefined ?? null;
|
||||
this.uiInputSystem = context.uiInputSystem as UIInputSystem | undefined ?? null;
|
||||
|
||||
@@ -253,14 +256,17 @@ export class EngineService {
|
||||
this.renderSystem.setUIRenderDataProvider(this.uiRenderProvider);
|
||||
}
|
||||
|
||||
// 在编辑器模式下,动画和行为树系统默认禁用
|
||||
// In editor mode, animation and behavior tree systems are disabled by default
|
||||
// 在编辑器模式下,动画、行为树和物理系统默认禁用
|
||||
// In editor mode, animation, behavior tree and physics systems are disabled by default
|
||||
if (this.animatorSystem) {
|
||||
this.animatorSystem.enabled = false;
|
||||
}
|
||||
if (this.behaviorTreeSystem) {
|
||||
this.behaviorTreeSystem.enabled = false;
|
||||
}
|
||||
if (this.physicsSystem) {
|
||||
this.physicsSystem.enabled = false;
|
||||
}
|
||||
|
||||
this.modulesInitialized = true;
|
||||
}
|
||||
@@ -289,6 +295,7 @@ export class EngineService {
|
||||
this.animatorSystem = null;
|
||||
this.tilemapSystem = null;
|
||||
this.behaviorTreeSystem = null;
|
||||
this.physicsSystem = null;
|
||||
this.uiRenderProvider = null;
|
||||
this.uiInputSystem = null;
|
||||
this.modulesInitialized = false;
|
||||
@@ -390,6 +397,11 @@ export class EngineService {
|
||||
if (this.behaviorTreeSystem) {
|
||||
this.behaviorTreeSystem.enabled = true;
|
||||
}
|
||||
// Enable physics system for preview
|
||||
// 启用物理系统用于预览
|
||||
if (this.physicsSystem) {
|
||||
this.physicsSystem.enabled = true;
|
||||
}
|
||||
this.startAutoPlayAnimations();
|
||||
|
||||
this.gameLoop();
|
||||
@@ -469,6 +481,14 @@ export class EngineService {
|
||||
if (this.behaviorTreeSystem) {
|
||||
this.behaviorTreeSystem.enabled = false;
|
||||
}
|
||||
// Disable and reset physics system
|
||||
// 禁用并重置物理系统
|
||||
if (this.physicsSystem) {
|
||||
this.physicsSystem.enabled = false;
|
||||
// Reset physics world state to prepare for next preview
|
||||
// 重置物理世界状态,为下次预览做准备
|
||||
this.physicsSystem.reset();
|
||||
}
|
||||
this.stopAllAnimations();
|
||||
|
||||
// Note: Don't cancel animationFrameId here, as renderLoop should keep running
|
||||
|
||||
@@ -86,12 +86,10 @@ export class RuntimeResolver {
|
||||
if (await this.hasRuntimeFilesInWorkspace(workspaceRoot)) {
|
||||
this.baseDir = workspaceRoot;
|
||||
this.isDev = true;
|
||||
console.log(`[RuntimeResolver] Using workspace dev files: ${this.baseDir}`);
|
||||
} else {
|
||||
// 回退到打包的资源目录(生产模式)
|
||||
this.baseDir = await TauriAPI.getAppResourceDir();
|
||||
this.isDev = false;
|
||||
console.log(`[RuntimeResolver] Using bundled resource dir: ${this.baseDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +99,7 @@ export class RuntimeResolver {
|
||||
*/
|
||||
private async hasRuntimeFilesInWorkspace(workspaceRoot: string): Promise<boolean> {
|
||||
const runtimePath = `${workspaceRoot}\\packages\\platform-web\\dist\\runtime.browser.js`;
|
||||
const exists = await TauriAPI.pathExists(runtimePath);
|
||||
console.log(`[RuntimeResolver] Checking workspace runtime: ${runtimePath} -> ${exists}`);
|
||||
return exists;
|
||||
return await TauriAPI.pathExists(runtimePath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,9 +205,6 @@ export class RuntimeResolver {
|
||||
* 生产模式:从编辑器内置资源复制
|
||||
*/
|
||||
async prepareRuntimeFiles(targetDir: string): Promise<void> {
|
||||
console.log(`[RuntimeResolver] Preparing runtime files to: ${targetDir}`);
|
||||
console.log(`[RuntimeResolver] isDev: ${this.isDev}, baseDir: ${this.baseDir}`);
|
||||
|
||||
// Ensure target directory exists
|
||||
const dirExists = await TauriAPI.pathExists(targetDir);
|
||||
if (!dirExists) {
|
||||
@@ -220,16 +213,13 @@ export class RuntimeResolver {
|
||||
|
||||
// Copy platform-web runtime
|
||||
const platformWeb = await this.getModuleFiles('platform-web');
|
||||
console.log(`[RuntimeResolver] platform-web files:`, platformWeb.files);
|
||||
for (const srcFile of platformWeb.files) {
|
||||
const filename = srcFile.split(/[/\\]/).pop() || '';
|
||||
const dstFile = `${targetDir}\\${filename}`;
|
||||
|
||||
const srcExists = await TauriAPI.pathExists(srcFile);
|
||||
console.log(`[RuntimeResolver] Copying ${srcFile} -> ${dstFile} (src exists: ${srcExists})`);
|
||||
if (srcExists) {
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
console.log(`[RuntimeResolver] Copied ${filename}`);
|
||||
} else {
|
||||
throw new Error(`Runtime file not found: ${srcFile}`);
|
||||
}
|
||||
@@ -237,22 +227,17 @@ export class RuntimeResolver {
|
||||
|
||||
// Copy engine WASM files
|
||||
const engine = await this.getModuleFiles('engine');
|
||||
console.log(`[RuntimeResolver] engine files:`, engine.files);
|
||||
for (const srcFile of engine.files) {
|
||||
const filename = srcFile.split(/[/\\]/).pop() || '';
|
||||
const dstFile = `${targetDir}\\${filename}`;
|
||||
|
||||
const srcExists = await TauriAPI.pathExists(srcFile);
|
||||
console.log(`[RuntimeResolver] Copying ${srcFile} -> ${dstFile} (src exists: ${srcExists})`);
|
||||
if (srcExists) {
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
console.log(`[RuntimeResolver] Copied ${filename}`);
|
||||
} else {
|
||||
throw new Error(`Engine file not found: ${srcFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[RuntimeResolver] Runtime files prepared successfully`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -511,7 +511,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--z-index-sticky);
|
||||
z-index: var(--z-index-dropdown);
|
||||
}
|
||||
|
||||
.component-dropdown {
|
||||
@@ -522,7 +522,7 @@
|
||||
border: 1px solid var(--color-border-strong);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
z-index: var(--z-index-dropdown);
|
||||
z-index: calc(var(--z-index-dropdown) + 1);
|
||||
overflow: hidden;
|
||||
animation: dropdownSlide 0.15s ease;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user