99import numpy as np
1010
1111from unilab .base .backend import SimBackend
12- from unilab .base .base import ABEnv , EnvCfg
12+ from unilab .base .base import ABEnv , EnvCfg , EnvPlayCapabilities
1313from unilab .base .dtype_config import get_global_dtype
1414from unilab .dr import DomainRandomizationManager , DomainRandomizationProvider
1515
@@ -200,6 +200,40 @@ def _compute_truncated(self, state: NpEnvState) -> np.ndarray:
200200 np .greater_equal (state .info ["steps" ], self ._cfg .max_episode_steps , out = truncated )
201201 return truncated
202202
203+ def init_play_renderer (self , render_spacing : float | None = None ) -> None :
204+ """Initialize backend-native interactive playback when available."""
205+ if not self .play_capabilities .supports_native_interactive_renderer :
206+ raise NotImplementedError (
207+ f"{ self ._backend .__class__ .__name__ } does not support native interactive playback"
208+ )
209+ if render_spacing is None :
210+ self ._backend .init_renderer ()
211+ return
212+ try :
213+ self ._backend .init_renderer (spacing = render_spacing )
214+ except TypeError :
215+ self ._backend .init_renderer ()
216+
217+ def render_play_frame (self ) -> None :
218+ """Render one interactive playback frame through the env contract."""
219+ if not self .play_capabilities .supports_native_interactive_renderer :
220+ raise NotImplementedError (
221+ f"{ self ._backend .__class__ .__name__ } does not support native interactive playback"
222+ )
223+ self ._backend .render ()
224+
225+ def get_physics_state_snapshot (self ) -> np .ndarray :
226+ """Return a detached physics snapshot for offline playback/video export."""
227+ if not self .play_capabilities .supports_physics_state_playback :
228+ raise NotImplementedError (
229+ f"{ self ._backend .__class__ .__name__ } does not support physics-state playback"
230+ )
231+ physics_state = cast (
232+ np .ndarray , np .asarray (self ._backend .get_physics_state (), dtype = np .float32 )
233+ )
234+ snapshot = cast (np .ndarray , physics_state .copy ())
235+ return snapshot
236+
203237 @abc .abstractmethod
204238 def apply_action (self , actions : np .ndarray , state : NpEnvState ) -> np .ndarray :
205239 """子类实现:action → ctrl"""
@@ -208,6 +242,22 @@ def apply_action(self, actions: np.ndarray, state: NpEnvState) -> np.ndarray:
208242 def update_state (self , state : NpEnvState ) -> NpEnvState :
209243 """子类实现:计算 obs/reward/terminated"""
210244
245+ @property
246+ def play_capabilities (self ) -> EnvPlayCapabilities :
247+ capabilities = self ._backend .get_play_capabilities ()
248+ return EnvPlayCapabilities (
249+ supports_native_interactive_renderer = capabilities .supports_native_interactive_renderer ,
250+ supports_physics_state_playback = capabilities .supports_physics_state_playback ,
251+ )
252+
253+ def get_playback_model (self ) -> Any :
254+ if not self ._supports_backend_property ("model" ):
255+ raise NotImplementedError (f"{ self .__class__ .__name__ } does not expose a playback model" )
256+ return self ._backend .model
257+
211258 def close (self ) -> None :
212259 """关闭环境"""
213260 pass
261+
262+ def _supports_backend_property (self , name : str ) -> bool :
263+ return hasattr (self ._backend , name )
0 commit comments