feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 (#244)

* feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题

* fix: 修复 CI 流程并清理代码
This commit is contained in:
YHH
2025-11-28 10:32:28 +08:00
committed by GitHub
parent cabb625a17
commit 673f5e5855
56 changed files with 4934 additions and 218 deletions

View File

@@ -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
};
});

View File

@@ -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) {

View File

@@ -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' }
];

View File

@@ -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>

View File

@@ -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

View File

@@ -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`);
}
/**

View File

@@ -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;
}