Skip to main content

spine/
spine.rs

1use quaso::{
2    GameLauncher,
3    animation::spine::{
4        BudgetedSpineSkeleton, BudgetedSpineSkeletonLodSwitchStrategy, LodSpineSkeleton,
5        SpineSkeleton,
6    },
7    assets::{make_directory_database, shader::ShaderAsset, spine::SpineAsset},
8    config::Config,
9    context::GameContext,
10    game::{GameInstance, GameState, GameStateChange},
11    third_party::{
12        spitfire_draw::utils::Drawable,
13        spitfire_glow::graphics::{CameraScaling, Shader},
14        spitfire_input::{
15            CardinalInputCombinator, InputActionRef, InputConsume, InputMapping, VirtualAction,
16        },
17        vek::Vec2,
18        windowing::event::VirtualKeyCode,
19    },
20};
21use std::error::Error;
22
23const SPEED: f32 = 200.0;
24
25fn main() -> Result<(), Box<dyn Error>> {
26    GameLauncher::new(GameInstance::new(Preloader).setup_assets(|assets| {
27        *assets = make_directory_database("./resources/").unwrap();
28    }))
29    .title("Spine 2D")
30    .config(Config::load_from_file("./resources/GameConfig.toml")?)
31    .run();
32    Ok(())
33}
34
35#[derive(Default)]
36struct Preloader;
37
38impl GameState for Preloader {
39    fn enter(&mut self, context: GameContext) {
40        context.graphics.state.color = [0.2, 0.2, 0.2, 1.0];
41        context.graphics.state.main_camera.screen_alignment = 0.5.into();
42        context.graphics.state.main_camera.scaling = CameraScaling::FitVertical(500.0);
43
44        context
45            .assets
46            .spawn(
47                "shader://color",
48                (ShaderAsset::new(
49                    Shader::COLORED_VERTEX_2D,
50                    Shader::PASS_FRAGMENT,
51                ),),
52            )
53            .unwrap();
54        context
55            .assets
56            .spawn(
57                "shader://image",
58                (ShaderAsset::new(
59                    Shader::TEXTURED_VERTEX_2D,
60                    Shader::TEXTURED_FRAGMENT,
61                ),),
62            )
63            .unwrap();
64        context
65            .assets
66            .spawn(
67                "shader://text",
68                (ShaderAsset::new(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT),),
69            )
70            .unwrap();
71
72        context.assets.ensure("spine://robot-lod0.zip").unwrap();
73        context.assets.ensure("spine://robot-lod1.zip").unwrap();
74
75        *context.state_change = GameStateChange::Swap(Box::new(State::default()));
76    }
77}
78
79#[derive(Default)]
80struct State {
81    skeleton: Option<BudgetedSpineSkeleton>,
82    movement: CardinalInputCombinator,
83    lod0: InputActionRef,
84    lod1: InputActionRef,
85}
86
87impl GameState for State {
88    fn enter(&mut self, context: GameContext) {
89        // Load Spine skeleton LODs assets.
90        let asset_lod0 = context
91            .assets
92            .find("spine://robot-lod0.zip")
93            .unwrap()
94            .access::<&SpineAsset>(context.assets);
95        let asset_lod1 = context
96            .assets
97            .find("spine://robot-lod1.zip")
98            .unwrap()
99            .access::<&SpineAsset>(context.assets);
100
101        // Create Spine skeleton instances for each LOD.
102        let lod0 = SpineSkeleton::new(asset_lod0);
103        // Since we start with LOD 0, we need to play animation on this LOD.
104        lod0.play_animation("idle", 0, 0.75, true).unwrap();
105        let lod1 = SpineSkeleton::new(asset_lod1);
106
107        // Create and setup budgeted Spine skeleton.
108        self.skeleton = Some(
109            BudgetedSpineSkeleton::default()
110                .lod_switch_strategy(BudgetedSpineSkeletonLodSwitchStrategy {
111                    // Since skeleton is playing animations, we need to transfer
112                    // just root bone transform to make new LOD be at the exact
113                    // place as old LOD was.
114                    transfer_root_bone_transform: true,
115                    // Make sure that when LODs are switched, same animation is
116                    // running on new LOD as it was on old LOD.
117                    synchronize_animations: true,
118                    ..Default::default()
119                })
120                // High quality skeleton with IK and physics animations.
121                .with_lod(LodSpineSkeleton {
122                    skeleton: lod0,
123                    refresh_delay: 0.0,
124                })
125                // Low quality skeleton with simple bone transform animations to
126                // make animation process faster.
127                .with_lod(LodSpineSkeleton {
128                    skeleton: lod1,
129                    // We also run it at lower frequency.
130                    refresh_delay: 0.05,
131                }),
132        );
133
134        // Setup inputs for moving the skeleton and switching LODs.
135        let move_left = InputActionRef::default();
136        let move_right = InputActionRef::default();
137        let move_up = InputActionRef::default();
138        let move_down = InputActionRef::default();
139        self.lod0 = InputActionRef::default();
140        self.lod1 = InputActionRef::default();
141        self.movement = CardinalInputCombinator::new(
142            move_left.clone(),
143            move_right.clone(),
144            move_up.clone(),
145            move_down.clone(),
146        );
147        context.input.push_mapping(
148            InputMapping::default()
149                .consume(InputConsume::Hit)
150                .action(
151                    VirtualAction::KeyButton(VirtualKeyCode::A),
152                    move_left.clone(),
153                )
154                .action(
155                    VirtualAction::KeyButton(VirtualKeyCode::D),
156                    move_right.clone(),
157                )
158                .action(VirtualAction::KeyButton(VirtualKeyCode::W), move_up.clone())
159                .action(
160                    VirtualAction::KeyButton(VirtualKeyCode::S),
161                    move_down.clone(),
162                )
163                .action(VirtualAction::KeyButton(VirtualKeyCode::Left), move_left)
164                .action(VirtualAction::KeyButton(VirtualKeyCode::Right), move_right)
165                .action(VirtualAction::KeyButton(VirtualKeyCode::Up), move_up)
166                .action(VirtualAction::KeyButton(VirtualKeyCode::Down), move_down)
167                .action(
168                    VirtualAction::KeyButton(VirtualKeyCode::Key1),
169                    self.lod0.clone(),
170                )
171                .action(
172                    VirtualAction::KeyButton(VirtualKeyCode::Key2),
173                    self.lod1.clone(),
174                ),
175        );
176    }
177
178    fn exit(&mut self, context: GameContext) {
179        context.input.pop_mapping();
180    }
181
182    fn fixed_update(&mut self, _: GameContext, delta_time: f32) {
183        let Some(budgeted_skeleton) = self.skeleton.as_mut() else {
184            return;
185        };
186
187        // Switch LODs if user trigger input actions.
188        if self.lod0.get().is_pressed() {
189            budgeted_skeleton.set_lod(0);
190        } else if self.lod1.get().is_pressed() {
191            budgeted_skeleton.set_lod(1);
192        }
193
194        // Update skeleton root bone transform based on user movement input.
195        if let Some(skeleton) = budgeted_skeleton.lod_skeleton_mut() {
196            let movement = Vec2::<f32>::from(self.movement.get());
197            skeleton
198                .skeleton
199                .update_local_transform(None, false, |position, _, _| {
200                    position.x += movement.x * SPEED * delta_time;
201                    position.y += movement.y * SPEED * delta_time;
202                });
203        };
204        // Update skeleton state based on its refresh frequency.
205        budgeted_skeleton.try_refresh(delta_time);
206    }
207
208    fn draw(&mut self, context: GameContext) {
209        let Some(skeleton) = self.skeleton.as_ref() else {
210            return;
211        };
212        skeleton.draw(context.draw, context.graphics);
213    }
214}