From 585b0f0093a3c0577821bcda6c7d33bc27497d33 Mon Sep 17 00:00:00 2001 From: badlogic Date: Fri, 21 Jun 2019 18:01:57 +0200 Subject: [PATCH] [xna][monogame] Rewrote example project, added mix-and-match example, see #1375. --- spine-csharp/spine-csharp.csproj | 2 +- spine-monogame/example/ExampleGame.cs | 383 +++++++++++++++++--------- 2 files changed, 249 insertions(+), 136 deletions(-) diff --git a/spine-csharp/spine-csharp.csproj b/spine-csharp/spine-csharp.csproj index 50792c8fa..40f697f02 100644 --- a/spine-csharp/spine-csharp.csproj +++ b/spine-csharp/spine-csharp.csproj @@ -71,11 +71,11 @@ + - diff --git a/spine-monogame/example/ExampleGame.cs b/spine-monogame/example/ExampleGame.cs index 1aa562018..637940bc6 100644 --- a/spine-monogame/example/ExampleGame.cs +++ b/spine-monogame/example/ExampleGame.cs @@ -33,15 +33,252 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace Spine { + + public abstract class Screen { + protected Example game; + protected SkeletonRenderer skeletonRenderer; + private MouseState lastMouseState; + protected Boolean mouseClicked = false; + + public Screen(Example game) { + this.game = game; + skeletonRenderer = new SkeletonRenderer(game.GraphicsDevice); + skeletonRenderer.PremultipliedAlpha = false; + } + + public void UpdateInput() { + MouseState state = Mouse.GetState(); + mouseClicked = lastMouseState.LeftButton == ButtonState.Pressed && state.LeftButton == ButtonState.Released; + lastMouseState = state; + } + + public abstract void Render(float deltaTime); + } + + /// + /// The raptor screen shows basic loading and rendering of a Spine skeleton. + /// + internal class RaptorScreen : Screen { + Atlas atlas; + Skeleton skeleton; + AnimationState state; + + public RaptorScreen(Example game) : base (game) { + // Load the texture atlas + atlas = new Atlas("data/raptor.atlas", new XnaTextureLoader(game.GraphicsDevice)); + + // Load the .json file using a scale of 0.5 + SkeletonJson json = new SkeletonJson(atlas); + json.Scale = 0.5f; + SkeletonData skeletonData = json.ReadSkeletonData("data/raptor-pro.json"); + + // Create the skeleton and animation state + skeleton = new Skeleton(skeletonData); + AnimationStateData stateData = new AnimationStateData(skeleton.Data); + state = new AnimationState(stateData); + + // Flip the skeleton on the y-axis and center it within the viewport + skeleton.ScaleY = -1; + skeleton.X = game.GraphicsDevice.Viewport.Width / 2; + skeleton.Y = game.GraphicsDevice.Viewport.Height; + + // Set the "walk" animation on track one and let it loop forever + state.SetAnimation(0, "walk", true); + } + + public override void Render(float deltaTime) { + // Update the animation state and apply the animations + // to the skeleton + state.Update(deltaTime); + state.Apply(skeleton); + + // Update the transformations of bones and other parts of the skeleton + skeleton.UpdateWorldTransform(); + + // Clear the screen and setup the projection matrix of the skeleton renderer + game.GraphicsDevice.Clear(Color.Black); + ((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, game.GraphicsDevice.Viewport.Width, game.GraphicsDevice.Viewport.Height, 0, 1, 0); + + // Draw the skeletons + skeletonRenderer.Begin(); + skeletonRenderer.Draw(skeleton); + skeletonRenderer.End(); + + // Check if the mouse button was clicked and switch scene + if (mouseClicked) game.currentScreen = new TankScreen(game); + } + } + + /// + /// The tank screen shows how to enable two color tinting. + /// + internal class TankScreen : Screen { + Atlas atlas; + Skeleton skeleton; + AnimationState state; + + public TankScreen(Example game) : base(game) { + // Instantiate and configure the two color tinting effect and + // assign it to the skeleton renderer + var twoColorTintEffect = game.Content.Load("Content\\SpineEffect"); + twoColorTintEffect.Parameters["World"].SetValue(Matrix.Identity); + twoColorTintEffect.Parameters["View"].SetValue(Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 1.0f), Vector3.Zero, Vector3.Up)); + skeletonRenderer.Effect = twoColorTintEffect; + + // The remaining code loads the atlas and skeleton data as in the raptor screen + atlas = new Atlas("data/tank.atlas", new XnaTextureLoader(game.GraphicsDevice)); + SkeletonJson json = new SkeletonJson(atlas); + json.Scale = 0.25f; + SkeletonData skeletonData = json.ReadSkeletonData("data/tank-pro.json"); + + skeleton = new Skeleton(skeletonData); + AnimationStateData stateData = new AnimationStateData(skeleton.Data); + state = new AnimationState(stateData); + + skeleton.ScaleY = -1; + skeleton.X = game.GraphicsDevice.Viewport.Width / 2 + 200; + skeleton.Y = game.GraphicsDevice.Viewport.Height; + + state.SetAnimation(0, "shoot", true); + } + + public override void Render(float deltaTime) { + state.Update(deltaTime); + state.Apply(skeleton); + + skeleton.UpdateWorldTransform(); + + // Clear the screen and setup the projection matrix of the custom effect through the + // "Projection" parameter. + game.GraphicsDevice.Clear(Color.Black); + skeletonRenderer.Effect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, game.GraphicsDevice.Viewport.Width, game.GraphicsDevice.Viewport.Height, 0, 1, 0)); + + skeletonRenderer.Begin(); + skeletonRenderer.Draw(skeleton); + skeletonRenderer.End(); + + if (mouseClicked) game.currentScreen = new SpineboyScreen(game); + } + } + + /// + /// The Spineboy screen shows how to queue up multiple animations via animation state, + /// set the default mix time to smoothly transition between animations, and load a + /// skeleton from a binary .skel file. + /// + internal class SpineboyScreen : Screen { + Atlas atlas; + Skeleton skeleton; + AnimationState state; + + public SpineboyScreen(Example game) : base(game) { + atlas = new Atlas("data/spineboy.atlas", new XnaTextureLoader(game.GraphicsDevice)); + + SkeletonBinary binary = new SkeletonBinary(atlas); + binary.Scale = 0.5f; + SkeletonData skeletonData = binary.ReadSkeletonData("data/spineboy-pro.skel"); + + skeleton = new Skeleton(skeletonData); + AnimationStateData stateData = new AnimationStateData(skeleton.Data); + state = new AnimationState(stateData); + + skeleton.ScaleY = -1; + skeleton.X = game.GraphicsDevice.Viewport.Width / 2; + skeleton.Y = game.GraphicsDevice.Viewport.Height; + + // We want 0.2 seconds of mixing time when transitioning from + // any animation to any other animation. + stateData.DefaultMix = 0.2f; + + // Set the "walk" animation on track one and let it loop forever + state.SetAnimation(0, "walk", true); + + // Queue another animation after 2 seconds to let Spineboy jump + state.AddAnimation(0, "jump", false, 2); + + // After the jump is complete, let Spineboy walk + state.AddAnimation(0, "run", true, 0); + } + + public override void Render(float deltaTime) { + state.Update(deltaTime); + state.Apply(skeleton); + skeleton.UpdateWorldTransform(); + + game.GraphicsDevice.Clear(Color.Black); + ((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, game.GraphicsDevice.Viewport.Width, game.GraphicsDevice.Viewport.Height, 0, 1, 0); + + skeletonRenderer.Begin(); + skeletonRenderer.Draw(skeleton); + skeletonRenderer.End(); + + if (mouseClicked) game.currentScreen = new MixAndMatchScreen(game); + } + } + + /// + /// The mix-and-match screen demonstrates how to create and apply a skin + /// composed of other skins. This method can be used to create customizable + /// avatar systems. + /// + internal class MixAndMatchScreen : Screen { + Atlas atlas; + Skeleton skeleton; + AnimationState state; + + public MixAndMatchScreen(Example game) : base(game) { + atlas = new Atlas("data/mix-and-match.atlas", new XnaTextureLoader(game.GraphicsDevice)); + + SkeletonJson json = new SkeletonJson(atlas); + json.Scale = 0.5f; + SkeletonData skeletonData = json.ReadSkeletonData("data/mix-and-match-pro.json"); + + skeleton = new Skeleton(skeletonData); + AnimationStateData stateData = new AnimationStateData(skeleton.Data); + state = new AnimationState(stateData); + + skeleton.ScaleY = -1; + skeleton.X = game.GraphicsDevice.Viewport.Width / 2; + skeleton.Y = game.GraphicsDevice.Viewport.Height; + + state.SetAnimation(0, "dance", true); + + // Create a new skin, by mixing and matching other skins + // that fit together. Items making up the girl are individual + // skins. Using the skin API, a new skin is created which is + // a combination of all these individual item skins. + var mixAndMatchSkin = new Spine.Skin("custom-girl"); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("skin-base")); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("nose/short")); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("eyes/eyelids-girly")); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("eyes/violet")); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("hair/brown")); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("clothes/hoodie-orange")); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("legs/pants-jeans")); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("accessories/bag")); + mixAndMatchSkin.AddSkin(skeletonData.FindSkin("accessories/hat-red-yellow")); + skeleton.SetSkin(mixAndMatchSkin); + } + + public override void Render(float deltaTime) { + state.Update(deltaTime); + state.Apply(skeleton); + skeleton.UpdateWorldTransform(); + + game.GraphicsDevice.Clear(Color.Black); + ((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, game.GraphicsDevice.Viewport.Width, game.GraphicsDevice.Viewport.Height, 0, 1, 0); + + skeletonRenderer.Begin(); + skeletonRenderer.Draw(skeleton); + skeletonRenderer.End(); + + if (mouseClicked) game.currentScreen = new RaptorScreen(game); + } + } + public class Example : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; - SkeletonRenderer skeletonRenderer; - Skeleton skeleton; - Slot headSlot; - AnimationState state; - SkeletonBounds bounds = new SkeletonBounds(); - - private string assetsFolder = "data/"; + public Screen currentScreen; public Example() { IsMouseVisible = true; @@ -53,139 +290,15 @@ namespace Spine { } protected override void LoadContent() { - // Two color tint effect, comment line 80 to disable - var spineEffect = Content.Load("Content\\SpineEffect"); - spineEffect.Parameters["World"].SetValue(Matrix.Identity); - spineEffect.Parameters["View"].SetValue(Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 1.0f), Vector3.Zero, Vector3.Up)); - - skeletonRenderer = new SkeletonRenderer(GraphicsDevice); - skeletonRenderer.PremultipliedAlpha = false; - skeletonRenderer.Effect = spineEffect; - - // String name = "spineboy-ess"; - // String name = "goblins-pro"; - String name = "raptor-pro"; - // String name = "tank-pro"; - // String name = "coin-pro"; - String atlasName = name.Replace("-pro", "").Replace("-ess", ""); - bool binaryData = false; - - Atlas atlas = new Atlas(assetsFolder + atlasName + ".atlas", new XnaTextureLoader(GraphicsDevice)); - - float scale = 1; - if (name == "spineboy-ess") scale = 0.6f; - if (name == "raptor-pro") scale = 0.5f; - if (name == "tank-pro") scale = 0.3f; - if (name == "coin-pro") scale = 1; - - SkeletonData skeletonData; - if (binaryData) { - SkeletonBinary binary = new SkeletonBinary(atlas); - binary.Scale = scale; - skeletonData = binary.ReadSkeletonData(assetsFolder + name + ".skel"); - } - else { - SkeletonJson json = new SkeletonJson(atlas); - json.Scale = scale; - skeletonData = json.ReadSkeletonData(assetsFolder + name + ".json"); - } - skeleton = new Skeleton(skeletonData); - if (name == "goblins-pro") skeleton.SetSkin("goblin"); - - // Define mixing between animations. - AnimationStateData stateData = new AnimationStateData(skeleton.Data); - state = new AnimationState(stateData); - - if (name == "spineboy-ess") { - skeleton.SetAttachment("head-bb", "head"); // Activate the head BoundingBoxAttachment. - - stateData.SetMix("run", "jump", 0.2f); - stateData.SetMix("jump", "run", 0.4f); - - // Event handling for all animations. - state.Start += Start; - state.End += End; - state.Complete += Complete; - state.Event += Event; - - state.SetAnimation(0, "run", false); - TrackEntry entry = state.AddAnimation(0, "jump", false, 0); - entry.End += End; // Event handling for queued animations. - state.AddAnimation(0, "run", true, 0); - } - else if (name == "raptor-pro") { - state.SetAnimation(0, "walk", true); - state.AddAnimation(1, "gun-grab", false, 2); - } - else if (name == "coin-pro") { - state.SetAnimation(0, "animation", true); - } - else if (name == "tank-pro") { - state.SetAnimation(0, "drive", true); - } - else { - state.SetAnimation(0, "walk", true); - } - - skeleton.X = 400 + (name == "tank-pro" ? 300 : 0); - skeleton.Y = GraphicsDevice.Viewport.Height; - skeleton.ScaleY = -1; - skeleton.UpdateWorldTransform(); - - headSlot = skeleton.FindSlot("head"); + currentScreen = new MixAndMatchScreen(this); } protected override void Update(GameTime gameTime) { - base.Update(gameTime); + currentScreen.UpdateInput(); } protected override void Draw(GameTime gameTime) { - GraphicsDevice.Clear(Color.Black); - - state.Update(gameTime.ElapsedGameTime.Milliseconds / 1000f); - state.Apply(skeleton); - skeleton.UpdateWorldTransform(); - if (skeletonRenderer.Effect is BasicEffect) { - ((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1, 0); - } - else { - skeletonRenderer.Effect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 1, 0)); - } - skeletonRenderer.Begin(); - skeletonRenderer.Draw(skeleton); - skeletonRenderer.End(); - - bounds.Update(skeleton, true); - MouseState mouse = Mouse.GetState(); - if (headSlot != null) { - headSlot.G = 1; - headSlot.B = 1; - if (bounds.AabbContainsPoint(mouse.X, mouse.Y)) { - BoundingBoxAttachment hit = bounds.ContainsPoint(mouse.X, mouse.Y); - if (hit != null) { - headSlot.G = 0; - headSlot.B = 0; - } - } - } - - base.Draw(gameTime); - } - - public void Start(TrackEntry entry) { - Console.WriteLine(entry + ": start"); - } - - public void End(TrackEntry entry) { - Console.WriteLine(entry + ": end"); - } - - public void Complete(TrackEntry entry) { - Console.WriteLine(entry + ": complete "); - } - - public void Event(TrackEntry entry, Event e) { - Console.WriteLine(entry + ": event " + e); - } + currentScreen.Render(gameTime.ElapsedGameTime.Milliseconds / 1000.0f); + } } }