spine/
spine.rs

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