-
- `;
+ `)
- // Setup the scene renderer and OpenGL context
- this.canvas = findWithClass(parent, "spine-player-canvas")[0] as HTMLCanvasElement;
- var webglConfig = { alpha: config.alpha };
- this.context = new spine.webgl.ManagedWebGLRenderingContext(this.canvas, webglConfig);
+ try {
+ // Validate the configuration
+ this.config = this.validateConfig(config);
+ } catch (e) {
+ this.showError(e);
+ return dom
+ }
- // Setup the scene renderer and loading screen
- this.sceneRenderer = new spine.webgl.SceneRenderer(this.canvas, this.context, true);
- this.loadingScreen = new spine.webgl.LoadingScreen(this.sceneRenderer);
+ try {
+ // Setup the scene renderer and OpenGL context
+ this.canvas = findWithClass(dom, "spine-player-canvas")[0] as HTMLCanvasElement;
+ var webglConfig = { alpha: config.alpha };
+ this.context = new spine.webgl.ManagedWebGLRenderingContext(this.canvas, webglConfig);
+ // Setup the scene renderer and loading screen
+ this.sceneRenderer = new spine.webgl.SceneRenderer(this.canvas, this.context, true);
+ this.loadingScreen = new spine.webgl.LoadingScreen(this.sceneRenderer);
+ } catch (e) {
+ this.showError("Sorry, your browser does not support WebGL.
Please use the latest version of Firefox, Chrome, Edge, or Safari.");
+ return dom;
+ }
// Load the assets
this.assetManager = new spine.webgl.AssetManager(this.context);
this.assetManager.loadText(config.jsonUrl);
this.assetManager.loadTextureAtlas(config.atlasUrl);
+ if (config.backgroundImage && config.backgroundImage.url)
+ this.assetManager.loadTexture(config.backgroundImage.url);
// Setup rendering loop
requestAnimationFrame(() => this.drawFrame());
// Setup the event listeners for UI elements
- let timeline = findWithClass(parent, "spine-player-timeline")[0];
- this.timelineSlider = new Slider(timeline);
- this.playButton = findWithId(parent, "spine-player-button-play-pause")[0];
- let speedButton = findWithId(parent, "spine-player-button-speed")[0];
- let animationButton = findWithId(parent, "spine-player-button-animation")[0];
- let skinButton = findWithId(parent, "spine-player-button-skin")[0];
- let settingsButton = findWithId(parent, "spine-player-button-settings")[0];
- let fullscreenButton = findWithId(parent, "spine-player-button-fullscreen")[0];
-
- let dropdown = findWithClass(parent, "spine-player-dropdown-content")[0];
-
- var justClicked = false;
- let dismissDropdown = function (event: any) {
- if (justClicked) {
- justClicked = false;
- return;
- }
- if (!isContained(dropdown, event.target)) {
- dropdown.classList.add("spine-player-hidden");
- window.onclick = null;
- }
- }
+ this.playerControls = findWithClass(dom, "spine-player-controls")[0];
+ let timeline = findWithClass(dom, "spine-player-timeline")[0];
+ this.timelineSlider = new Slider();
+ timeline.appendChild(this.timelineSlider.render());
+ this.playButton = findWithId(dom, "spine-player-button-play-pause")[0];
+ let speedButton = findWithId(dom, "spine-player-button-speed")[0];
+ this.animationButton = findWithId(dom, "spine-player-button-animation")[0];
+ this.skinButton = findWithId(dom, "spine-player-button-skin")[0];
+ let settingsButton = findWithId(dom, "spine-player-button-settings")[0];
+ let fullscreenButton = findWithId(dom, "spine-player-button-fullscreen")[0];
this.playButton.onclick = () => {
if (this.paused) this.play()
@@ -230,124 +383,19 @@
}
speedButton.onclick = () => {
- dropdown.classList.remove("spine-player-hidden");
- dropdown.innerHTML = /*html*/`
-
- `;
- let sliderParent = findWithClass(dropdown, "spine-player-speed-slider")[0];
- let slider = new Slider(sliderParent);
- slider.setValue(this.speed / 2);
- slider.change = (percentage) => {
- this.speed = percentage * 2;
- }
- justClicked = true;
- window.onclick = dismissDropdown;
+ this.showSpeedDialog();
}
- animationButton.onclick = () => {
- if (!this.skeleton || this.skeleton.data.animations.length == 0) return;
- dropdown.classList.remove("spine-player-hidden");
- dropdown.innerHTML = /*html*/`
-
Animations
-
-
- `;
-
- let rows = findWithClass(dropdown, "spine-player-list")[0];
- this.skeleton.data.animations.forEach((animation) => {
- let row = document.createElement("li");
- row.classList.add("spine-player-list-item");
- if (animation.name == this.config.animation) row.classList.add("spine-player-list-item-selected");
- row.innerText = animation.name;
- rows.appendChild(row);
- row.onclick = () => {
- removeClass(rows.children, "spine-player-list-item-selected");
- row.classList.add("spine-player-list-item-selected");
- this.config.animation = animation.name;
- this.playTime = 0;
- this.animationState.setAnimation(0, this.config.animation, true);
- }
- });
-
- justClicked = true;
- window.onclick = dismissDropdown;
+ this.animationButton.onclick = () => {
+ this.showAnimationsDialog();
}
- skinButton.onclick = () => {
- if (!this.skeleton || this.skeleton.data.animations.length == 0) return;
- dropdown.classList.remove("spine-player-hidden");
- dropdown.innerHTML = /*html*/`
-
Skins
-
-
- `;
-
- let rows = findWithClass(dropdown, "spine-player-list")[0];
- this.skeleton.data.skins.forEach((skin) => {
- let row = document.createElement("li");
- row.classList.add("spine-player-list-item");
- if (skin.name == this.config.skin) row.classList.add("spine-player-list-item-selected");
- row.innerText = skin.name;
- rows.appendChild(row);
- row.onclick = () => {
- removeClass(rows.children, "spine-player-list-item-selected");
- row.classList.add("spine-player-list-item-selected");
- this.config.skin = skin.name;
- this.skeleton.setSkinByName(this.config.skin);
- this.skeleton.setSlotsToSetupPose();
- }
- });
-
- justClicked = true;
- window.onclick = dismissDropdown;
+ this.skinButton.onclick = () => {
+ this.showSkinsDialog();
}
settingsButton.onclick = () => {
- if (!this.skeleton || this.skeleton.data.animations.length == 0) return;
- dropdown.classList.remove("spine-player-hidden");
- dropdown.innerHTML = /*html*/`
-
Debug
-
-
-
- `;
-
- let rows = findWithClass(dropdown, "spine-player-list")[0];
- let makeItem = (name: string) => {
- let row = document.createElement("div");
- row.classList.add("spine-player-list-item");
- if ((this.config.debug as any)[name] == true) row.classList.add("spine-player-list-item-selected");
- row.innerText = name
- rows.appendChild(row);
- row.onclick = () => {
- if ((this.config.debug as any)[name]) {
- (this.config.debug as any)[name] = false;
- row.classList.remove("spine-player-list-item-selected");
- } else {
- (this.config.debug as any)[name] = true;
- row.classList.add("spine-player-list-item-selected");
- }
- }
- };
-
- Object.keys(this.config.debug).forEach((name) => {
- makeItem(name);
- });
-
- justClicked = true;
- window.onclick = dismissDropdown;
+ this.showSettingsDialog();
}
fullscreenButton.onclick = () => {
@@ -358,7 +406,7 @@
else if (doc.webkitExitFullscreen) doc.webkitExitFullscreen()
else if (doc.msExitFullscreen) doc.msExitFullscreen();
} else {
- let player = findWithClass(parent, "spine-player")[0] as any;
+ let player = dom as any;
if (player.requestFullscreen) player.requestFullscreen();
else if (player.webkitRequestFullScreen) player.webkitRequestFullScreen();
else if (player.mozRequestFullScreen) player.mozRequestFullScreen();
@@ -370,6 +418,143 @@
window.onresize = () => {
this.drawFrame(false);
}
+
+ return dom;
+ }
+
+ showSpeedDialog () {
+ let popup = new Popup(this.playerControls, /*html*/`
+
+ `);
+ let sliderParent = findWithClass(popup.dom, "spine-player-speed-slider")[0];
+ let slider = new Slider(2);
+ sliderParent.appendChild(slider.render());
+ slider.setValue(this.speed / 2);
+ slider.change = (percentage) => {
+ this.speed = percentage * 2;
+ }
+ popup.show();
+ }
+
+ showAnimationsDialog () {
+ if (!this.skeleton || this.skeleton.data.animations.length == 0) return;
+
+ let popup = new Popup(this.playerControls, /*html*/`
+
+
+
+ `);
+
+ let rows = findWithClass(popup.dom, "spine-player-list")[0];
+ this.skeleton.data.animations.forEach((animation) => {
+ // skip animations not whitelisted if a whitelist is given
+ if (this.config.animations && this.config.animations.indexOf(animation.name) < 0) {
+ return;
+ }
+
+ let row = createElement(/*html*/`
+
+
+
+
+
+
+ `);
+ if (animation.name == this.config.animation) row.classList.add("selected");
+ findWithClass(row, "selectable-text")[0].innerText = animation.name;
+ rows.appendChild(row);
+ row.onclick = () => {
+ removeClass(rows.children, "selected");
+ row.classList.add("selected");
+ this.config.animation = animation.name;
+ this.playTime = 0;
+ this.animationState.setAnimation(0, this.config.animation, true);
+ }
+ });
+ popup.show();
+ }
+
+ showSkinsDialog () {
+ if (!this.skeleton || this.skeleton.data.animations.length == 0) return;
+
+ let popup = new Popup(this.playerControls, /*html*/`
+
+
+
+ `);
+
+ let rows = findWithClass(popup.dom, "spine-player-list")[0];
+ this.skeleton.data.skins.forEach((skin) => {
+ // skip skins not whitelisted if a whitelist is given
+ if (this.config.skins && this.config.skins.indexOf(skin.name) < 0) {
+ return;
+ }
+
+ let row = createElement(/*html*/`
+
+
+
+
+
+
+ `);
+ if (skin.name == this.config.skin) row.classList.add("selected");
+ findWithClass(row, "selectable-text")[0].innerText = skin.name;
+ rows.appendChild(row);
+ row.onclick = () => {
+ removeClass(rows.children, "selected");
+ row.classList.add("selected");
+ this.config.skin = skin.name;
+ this.skeleton.setSkinByName(this.config.skin);
+ this.skeleton.setSlotsToSetupPose();
+ }
+ });
+
+ popup.show();
+ }
+
+ showSettingsDialog () {
+ if (!this.skeleton || this.skeleton.data.animations.length == 0) return;
+
+ let popup = new Popup(this.playerControls, /*html*/`
+
+
+
+
+ `);
+
+ let rows = findWithClass(popup.dom, "spine-player-list")[0];
+ let makeItem = (label: string, name: string) => {
+ let row = createElement(/*html*/``);
+ let s = new Switch(label);
+ row.appendChild(s.render());
+ s.setEnabled((this.config.debug as any)[name]);
+ s.change = (value) => {
+ (this.config.debug as any)[name] = value;
+ }
+ rows.appendChild(row);
+ };
+
+ makeItem("Bones", "bones");
+ makeItem("Regions", "regions");
+ makeItem("Meshes", "meshes");
+ makeItem("Bounds", "bounds");
+ makeItem("Paths", "paths");
+ makeItem("Clipping", "clipping");
+ makeItem("Points", "points");
+ makeItem("Hulls", "hulls");
+
+ popup.show();
}
drawFrame (requestNextFrame = true) {
@@ -393,13 +578,14 @@
// Update and draw the skeleton
if (this.loaded) {
+ // Update animation and skeleton based on user selections
if (!this.paused && this.config.animation) {
this.time.update();
let delta = this.time.delta * this.speed;
let animationDuration = this.animationState.getCurrent(0).animation.duration;
this.playTime += delta;
- while (this.playTime >= animationDuration) {
+ while (this.playTime >= animationDuration && animationDuration != 0) {
this.playTime -= animationDuration;
}
this.playTime = Math.max(0, Math.min(this.playTime, animationDuration));
@@ -407,9 +593,10 @@
this.animationState.update(delta);
this.animationState.apply(this.skeleton);
- this.skeleton.updateWorldTransform();
}
+ this.skeleton.updateWorldTransform();
+
let viewportSize = this.scale(this.config.viewport.width, this.config.viewport.height, this.canvas.width, this.canvas.height);
this.sceneRenderer.camera.zoom = this.config.viewport.width / viewportSize.x;
@@ -417,15 +604,43 @@
this.sceneRenderer.camera.position.y = this.config.viewport.y + this.config.viewport.height / 2;
this.sceneRenderer.begin();
+
+ // Draw background image if given
+ if (this.config.backgroundImage && this.config.backgroundImage.url) {
+ let bgImage = this.assetManager.get(this.config.backgroundImage.url);
+ if (!this.config.backgroundImage.x) {
+ this.sceneRenderer.drawTexture(bgImage, this.config.viewport.x, this.config.viewport.y, this.config.viewport.width, this.config.viewport.height);
+ } else {
+ this.sceneRenderer.drawTexture(bgImage, this.config.backgroundImage.x, this.config.backgroundImage.y, this.config.backgroundImage.width, this.config.backgroundImage.height);
+ }
+ }
+
+ // Draw skeleton and debug output
this.sceneRenderer.drawSkeleton(this.skeleton, this.config.premultipliedAlpha);
this.sceneRenderer.skeletonDebugRenderer.drawBones = this.config.debug.bones;
this.sceneRenderer.skeletonDebugRenderer.drawBoundingBoxes = this.config.debug.bounds;
this.sceneRenderer.skeletonDebugRenderer.drawClipping = this.config.debug.clipping;
- this.sceneRenderer.skeletonDebugRenderer.drawMeshHull = this.config.debug.meshHull;
+ this.sceneRenderer.skeletonDebugRenderer.drawMeshHull = this.config.debug.hulls;
this.sceneRenderer.skeletonDebugRenderer.drawPaths = this.config.debug.paths;
this.sceneRenderer.skeletonDebugRenderer.drawRegionAttachments = this.config.debug.regions;
- this.sceneRenderer.skeletonDebugRenderer.drawMeshTriangles = this.config.debug.triangles;
+ this.sceneRenderer.skeletonDebugRenderer.drawMeshTriangles = this.config.debug.meshes;
this.sceneRenderer.drawSkeletonDebug(this.skeleton, this.config.premultipliedAlpha);
+
+ // Render the selected bones
+ let controlBones = this.config.controlBones;
+ let selectedBones = this.selectedBones;
+ let skeleton = this.skeleton;
+ gl.lineWidth(2);
+ for (var i = 0; i < controlBones.length; i++) {
+ var bone = skeleton.findBone(controlBones[i]);
+ if (!bone) continue;
+ var colorInner = selectedBones[i] !== null ? SpinePlayer.HOVER_COLOR_INNER : SpinePlayer.NON_HOVER_COLOR_INNER;
+ var colorOuter = selectedBones[i] !== null ? SpinePlayer.HOVER_COLOR_OUTER : SpinePlayer.NON_HOVER_COLOR_OUTER;
+ this.sceneRenderer.circle(true, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorInner);
+ this.sceneRenderer.circle(false, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorOuter);
+ }
+ gl.lineWidth(1);
+
this.sceneRenderer.end();
this.sceneRenderer.camera.zoom = 0;
@@ -445,27 +660,61 @@
loadSkeleton () {
if (this.loaded) return;
+ if (this.assetManager.hasErrors()) {
+ this.showError("Error: assets could not be loaded.
" + escapeHtml(JSON.stringify(this.assetManager.getErrors())));
+ return;
+ }
+
let atlas = this.assetManager.get(this.config.atlasUrl);
let jsonText = this.assetManager.get(this.config.jsonUrl);
let json = new SkeletonJson(new AtlasAttachmentLoader(atlas));
- let skeletonData = json.readSkeletonData(jsonText);
+ let skeletonData: SkeletonData;
+ try {
+ skeletonData = json.readSkeletonData(jsonText);
+ } catch (e) {
+ this.showError("Error: could not load skeleton .json.
" + escapeHtml(JSON.stringify(e)));
+ return;
+ }
this.skeleton = new Skeleton(skeletonData);
let stateData = new AnimationStateData(skeletonData);
stateData.defaultMix = 0.2;
this.animationState = new AnimationState(stateData);
+ // Check if all controllable bones are in the skeleton
+ if (this.config.controlBones) {
+ this.config.controlBones.forEach(bone => {
+ if (!skeletonData.findBone(bone)) {
+ this.showError(`Error: control bone '${bone}' does not exist in skeleton.`);
+ }
+ })
+ }
+
// Setup skin
if (!this.config.skin) {
if (skeletonData.skins.length > 0) {
this.config.skin = skeletonData.skins[0].name;
}
}
+
+ if (this.config.skins && this.config.skin.length > 0) {
+ this.config.skins.forEach(skin => {
+ if (!this.skeleton.data.findSkin(skin)) {
+ this.showError(`Error: skin '${skin}' in selectable skin list does not exist in skeleton.`);
+ return;
+ }
+ });
+ }
+
if (this.config.skin) {
+ if (!this.skeleton.data.findSkin(this.config.skin)) {
+ this.showError(`Error: skin '${this.config.skin}' does not exist in skeleton.`);
+ return;
+ }
this.skeleton.setSkinByName(this.config.skin);
this.skeleton.setSlotsToSetupPose();
}
- // Setup viewport
+ // Setup viewport after skin is set
if (!this.config.viewport || !this.config.viewport.x || !this.config.viewport.y || !this.config.viewport.width || !this.config.viewport.height) {
this.config.viewport = {
x: 0,
@@ -484,13 +733,31 @@
this.config.viewport.height = size.y * 1.2;
}
- // Setup the first animation
+ // Setup the animations after viewport, so default bounds don't get messed up.
+ if (this.config.animations && this.config.animations.length > 0) {
+ this.config.animations.forEach(animation => {
+ if (!this.skeleton.data.findAnimation(animation)) {
+ this.showError(`Error: animation '${animation}' in selectable animation list does not exist in skeleton.`);
+ return;
+ }
+ });
+
+ if (!this.config.animation) {
+ this.config.animation = this.config.animations[0];
+ }
+ }
+
if (!this.config.animation) {
if (skeletonData.animations.length > 0) {
this.config.animation = skeletonData.animations[0].name;
}
}
+
if(this.config.animation) {
+ if (!skeletonData.findAnimation(this.config.animation)) {
+ this.showError(`Error: animation '${this.config.animation}' does not exist in skeleton.`);
+ return;
+ }
this.play()
this.timelineSlider.change = (percentage) => {
this.pause();
@@ -503,9 +770,107 @@
}
}
+ // Setup the input processor and controllable bones
+ this.setupInput();
+
+ // Hide skin and animation if there's only the default skin / no animation
+ if (skeletonData.skins.length == 1) this.skinButton.classList.add("spine-player-hidden");
+ if (skeletonData.animations.length == 1) this.animationButton.classList.add("spine-player-hidden");
+
+ this.config.success(this);
this.loaded = true;
}
+ setupInput () {
+ let controlBones = this.config.controlBones;
+ let selectedBones = this.selectedBones = new Array(this.config.controlBones.length);
+ let canvas = this.canvas;
+ let input = new spine.webgl.Input(canvas);
+ var target:Bone = null;
+ let coords = new spine.webgl.Vector3();
+ let temp = new spine.webgl.Vector3();
+ let temp2 = new spine.Vector2();
+ let skeleton = this.skeleton
+ let renderer = this.sceneRenderer;
+ input.addListener({
+ down: (x, y) => {
+ for (var i = 0; i < controlBones.length; i++) {
+ var bone = skeleton.findBone(controlBones[i]);
+ if (!bone) continue;
+ renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.width, canvas.height);
+ if (temp.set(skeleton.x + bone.worldX, skeleton.y + bone.worldY, 0).distance(coords) < 30) {
+ target = bone;
+ }
+ }
+ handleHover();
+ },
+ up: (x, y) => {
+ target = null;
+ handleHover();
+ },
+ dragged: (x, y) => {
+ if (target != null) {
+ renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.width, canvas.height);
+ if (target.parent !== null) {
+ target.parent.worldToLocal(temp2.set(coords.x - skeleton.x, coords.y - skeleton.y));
+ target.x = temp2.x;
+ target.y = temp2.y;
+ } else {
+ target.x = coords.x - skeleton.x;
+ target.y = coords.y - skeleton.y;
+ }
+ }
+ handleHover();
+ },
+ moved: (x, y) => {
+ for (var i = 0; i < controlBones.length; i++) {
+ var bone = skeleton.findBone(controlBones[i]);
+ if (!bone) continue;
+ renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.width, canvas.height);
+ if (temp.set(skeleton.x + bone.worldX, skeleton.y + bone.worldY, 0).distance(coords) < 30) {
+ selectedBones[i] = bone;
+ } else {
+ selectedBones[i] = null;
+ }
+ }
+ handleHover();
+ }
+ });
+
+ // For the manual hover to work, we need to disable
+ // hidding the controls if the mouse/touch entered
+ // the clickable area of a child of the controls
+ let mouseOverChildren = false;
+ canvas.onmouseover = (ev) => {
+ mouseOverChildren = false;
+ }
+ canvas.onmouseout = (ev) => {
+ if (ev.relatedTarget == null) {
+ mouseOverChildren = false;
+ } else {
+ mouseOverChildren = isContained(this.dom, (ev.relatedTarget as any));
+ }
+ }
+
+ let cancelId = 0;
+ let handleHover = () => {
+ if (!this.config.showControls) return;
+ clearTimeout(cancelId);
+ this.playerControls.classList.remove("hidden");
+ this.playerControls.classList.add("visible");
+ let remove = () => {
+ let popup = findWithClass(this.dom, "spine-player-popup");
+ if (popup.length == 0 && !mouseOverChildren) {
+ this.playerControls.classList.remove("visible");
+ this.playerControls.classList.add("hidden");
+ } else {
+ cancelId = setTimeout(remove, 1000);
+ }
+ };
+ cancelId = setTimeout(remove, 1000);
+ }
+ }
+
private play () {
this.paused = false;
this.playButton.classList.remove("spine-player-button-icon-play");
@@ -523,20 +888,6 @@
this.playButton.classList.remove("spine-player-button-icon-pause");
this.playButton.classList.add("spine-player-button-icon-play");
}
-
- private resize () {
- let canvas = this.canvas;
- let w = canvas.clientWidth;
- let h = canvas.clientHeight;
-
- var devicePixelRatio = window.devicePixelRatio || 1;
- if (canvas.width != Math.floor(w * devicePixelRatio) || canvas.height != Math.floor(h * devicePixelRatio)) {
- canvas.width = Math.floor(w * devicePixelRatio);
- canvas.height = Math.floor(h * devicePixelRatio);
- }
- this.context.gl.viewport(0, 0, canvas.width, canvas.height);
- this.sceneRenderer.camera.setViewport(canvas.width, canvas.height);
- }
}
function isContained(dom: HTMLElement, needle: HTMLElement): boolean {
@@ -578,9 +929,25 @@
return found;
}
+ function createElement(html: string): HTMLElement {
+ let dom = document.createElement("div");
+ dom.innerHTML = html;
+ return dom.children[0] as HTMLElement;
+ }
+
function removeClass(elements: HTMLCollection, clazz: string) {
for (var i = 0; i < elements.length; i++) {
elements[i].classList.remove(clazz);
}
}
+
+ function escapeHtml(str: string) {
+ if (!str) return "";
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
}
\ No newline at end of file
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
index 4ab24a88b..7988b2680 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
@@ -34,7 +34,6 @@ namespace Spine.Unity {
[ExecuteInEditMode]
[AddComponentMenu("Spine/SkeletonAnimation")]
- [HelpURL("http://esotericsoftware.com/spine-unity-documentation#Controlling-Animation")]
public class SkeletonAnimation : SkeletonRenderer, ISkeletonAnimation, IAnimationStateComponent {
#region IAnimationStateComponent
@@ -158,7 +157,7 @@ namespace Spine.Unity {
if (Application.isPlaying) {
#endif
- // Make this block not run in Unity Editor edit mode.
+ // In Unity Editor edit mode, make this block not run.
state.SetAnimation(0, animationObject, loop);
#if UNITY_EDITOR
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs
index 53bbc5a3f..641b42047 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs
@@ -35,12 +35,16 @@ using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
- /// Renders a skeleton.
+ /// Base class of animated Spine skeleton components. This component manages and renders a skeleton.
[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer)), DisallowMultipleComponent]
- [HelpURL("http://esotericsoftware.com/spine-unity-documentation#Rendering")]
+ [HelpURL("http://esotericsoftware.com/spine-unity-rendering")]
public class SkeletonRenderer : MonoBehaviour, ISkeletonComponent, IHasSkeletonDataAsset {
+ public bool logErrors = false;
+
public delegate void SkeletonRendererDelegate (SkeletonRenderer skeletonRenderer);
+
+ /// OnRebuild is raised after the Skeleton is successfully initialized.
public event SkeletonRendererDelegate OnRebuild;
/// Occurs after the vertex data is populated every frame, before the vertices are pushed into the mesh.
@@ -48,34 +52,59 @@ namespace Spine.Unity {
public SkeletonDataAsset skeletonDataAsset;
public SkeletonDataAsset SkeletonDataAsset { get { return skeletonDataAsset; } } // ISkeletonComponent
- [SpineSkin(defaultAsEmptyString:true)]
- public string initialSkinName;
- public bool initialFlipX, initialFlipY;
- #region Advanced
+ #region Initialization settings
+ /// Skin name to use when the Skeleton is initialized.
+ [SpineSkin(defaultAsEmptyString:true)] public string initialSkinName;
+
+ /// Flip X and Y to use when the Skeleton is initialized.
+ public bool initialFlipX, initialFlipY;
+ #endregion
+
+ #region Advanced Render Settings
// Submesh Separation
+ /// Slot names used to populate separatorSlots list when the Skeleton is initialized. Changing this after initialization does nothing.
[UnityEngine.Serialization.FormerlySerializedAs("submeshSeparators")][SerializeField][SpineSlot] protected string[] separatorSlotNames = new string[0];
+
+ /// Slots that determine where the render is split. This is used by components such as SkeletonRenderSeparator so that the skeleton can be rendered by two separate renderers on different GameObjects.
[System.NonSerialized] public readonly List separatorSlots = new List();
+ // Render Settings
[Range(-0.1f, 0f)] public float zSpacing;
+ /// Use Spine's clipping feature. If false, ClippingAttachments will be ignored.
public bool useClipping = true;
+
+ /// If true, triangles will not be updated. Enable this as an optimization if the skeleton does not make use of attachment swapping or hiding, or draw order keys. Otherwise, setting this to false may cause errors in rendering.
public bool immutableTriangles = false;
+
+ /// Multiply vertex color RGB with vertex color alpha. Set this to true if the shader used for rendering is a premultiplied alpha shader. Setting this to false disables single-batch additive slots.
public bool pmaVertexColors = true;
+
/// Clears the state of the render and skeleton when this component or its GameObject is disabled. This prevents previous state from being retained when it is enabled again. When pooling your skeleton, setting this to true can be helpful.
public bool clearStateOnDisable = false;
+
+ /// If true, second colors on slots will be added to the output Mesh as UV2 and UV3. A special "tint black" shader that interprets UV2 and UV3 as black point colors is required to render this properly.
public bool tintBlack = false;
+
+ /// If true, the renderer assumes the skeleton only requires one Material and one submesh to render. This allows the MeshGenerator to skip checking for changes in Materials. Enable this as an optimization if the skeleton only uses one Material.
+ /// This disables SkeletonRenderSeparator functionality.
public bool singleSubmesh = false;
- [UnityEngine.Serialization.FormerlySerializedAs("calculateNormals")]
- public bool addNormals = false;
+ /// If true, the mesh generator adds normals to the output mesh. For better performance and reduced memory requirements, use a shader that assumes the desired normal.
+ [UnityEngine.Serialization.FormerlySerializedAs("calculateNormals")] public bool addNormals = false;
+
+ /// If true, tangents are calculated every frame and added to the Mesh. Enable this when using a shader that uses lighting that requires tangents.
public bool calculateTangents = false;
+ #endregion
- public bool logErrors = false;
-
+ #region Overrides
#if SPINE_OPTIONAL_RENDEROVERRIDE
+ // These are API for anything that wants to take over rendering for a SkeletonRenderer.
public bool disableRenderingOnOverride = true;
public delegate void InstructionDelegate (SkeletonRendererInstruction instruction);
event InstructionDelegate generateMeshOverride;
+
+ /// Allows separate code to take over rendering for this SkeletonRenderer component. The subscriber is passed a SkeletonRendererInstruction argument to determine how to render a skeleton.
public event InstructionDelegate GenerateMeshOverride {
add {
generateMeshOverride += value;
@@ -96,17 +125,27 @@ namespace Spine.Unity {
#if SPINE_OPTIONAL_MATERIALOVERRIDE
[System.NonSerialized] readonly Dictionary customMaterialOverride = new Dictionary();
+ /// Use this Dictionary to override a Material with a different Material.
public Dictionary CustomMaterialOverride { get { return customMaterialOverride; } }
#endif
- // Custom Slot Material
[System.NonSerialized] readonly Dictionary customSlotMaterials = new Dictionary();
+ /// Use this Dictionary to use a different Material to render specific Slots.
public Dictionary CustomSlotMaterials { get { return customSlotMaterials; } }
#endregion
+ #region Mesh Generator
+ [System.NonSerialized] readonly SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction();
+ readonly MeshGenerator meshGenerator = new MeshGenerator();
+ [System.NonSerialized] readonly MeshRendererBuffers rendererBuffers = new MeshRendererBuffers();
+ #endregion
+
+ #region Cached component references
MeshRenderer meshRenderer;
MeshFilter meshFilter;
+ #endregion
+ #region Skeleton
[System.NonSerialized] public bool valid;
[System.NonSerialized] public Skeleton skeleton;
public Skeleton Skeleton {
@@ -115,10 +154,7 @@ namespace Spine.Unity {
return skeleton;
}
}
-
- [System.NonSerialized] readonly SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction();
- readonly MeshGenerator meshGenerator = new MeshGenerator();
- [System.NonSerialized] readonly MeshRendererBuffers rendererBuffers = new MeshRendererBuffers();
+ #endregion
#region Runtime Instantiation
public static T NewSpineGameObject (SkeletonDataAsset skeletonDataAsset) where T : SkeletonRenderer {
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Mesh Generation/SpineMesh.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Mesh Generation/SpineMesh.cs
index ea080b115..6a5691a35 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Mesh Generation/SpineMesh.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Mesh Generation/SpineMesh.cs
@@ -84,7 +84,6 @@ namespace Spine.Unity {
}
public delegate void MeshGeneratorDelegate (MeshGeneratorBuffers buffers);
-
public struct MeshGeneratorBuffers {
/// The vertex count that will actually be used for the mesh. The Lengths of the buffer arrays may be larger than this number.
public int vertexCount;
@@ -102,6 +101,7 @@ namespace Spine.Unity {
public MeshGenerator meshGenerator;
}
+ /// Holds several methods to prepare and generate a UnityEngine mesh based on a skeleton. Contains buffers needed to perform the operation, and serializes settings for mesh generation.
[System.Serializable]
public class MeshGenerator {
public Settings settings = Settings.Default;
@@ -152,6 +152,7 @@ namespace Spine.Unity {
[NonSerialized] int[] regionTriangles = { 0, 1, 2, 2, 3, 0 };
#region Optional Buffers
+ // These optional buffers are lazy-instantiated when the feature is used.
[NonSerialized] Vector3[] normals;
[NonSerialized] Vector4[] tangents;
[NonSerialized] Vector2[] tempTanBuffer;
@@ -161,6 +162,7 @@ namespace Spine.Unity {
public int VertexCount { get { return vertexBuffer.Count; } }
+ /// A set of mesh arrays whose values are modifiable by the user. Modify these values before they are passed to the UnityEngine mesh object in order to see the effect.
public MeshGeneratorBuffers Buffers {
get {
return new MeshGeneratorBuffers {
@@ -1093,6 +1095,7 @@ namespace Spine.Unity {
}
}
+ /// Trims internal buffers to reduce the resulting mesh data stream size.
public void TrimExcess () {
vertexBuffer.TrimExcess();
uvBuffer.TrimExcess();
@@ -1201,8 +1204,7 @@ namespace Spine.Unity {
static List AttachmentColors32 = new List();
static List AttachmentIndices = new List();
- ///
- /// Fills mesh vertex data to render a RegionAttachment.
+ /// Fills mesh vertex data to render a RegionAttachment.
public static void FillMeshLocal (Mesh mesh, RegionAttachment regionAttachment) {
if (mesh == null) return;
if (regionAttachment == null) return;
@@ -1308,11 +1310,10 @@ namespace Spine.Unity {
AttachmentColors32.Clear();
AttachmentIndices.Clear();
}
-
-
#endregion
}
+ /// A double-buffered Mesh, and a shared material array, bundled for use by Spine components that need to push a Mesh and materials to a Unity MeshRenderer and MeshFilter.
public class MeshRendererBuffers : IDisposable {
DoubleBuffered doubleBufferedMesh;
internal readonly ExposedList submeshMaterials = new ExposedList();
@@ -1328,6 +1329,8 @@ namespace Spine.Unity {
}
}
+ /// Returns a sharedMaterials array for use on a MeshRenderer.
+ ///
public Material[] GetUpdatedSharedMaterialsArray () {
if (submeshMaterials.Count == sharedMaterials.Length)
submeshMaterials.CopyTo(sharedMaterials);
@@ -1337,6 +1340,7 @@ namespace Spine.Unity {
return sharedMaterials;
}
+ /// Returns true if the materials were modified since the buffers were last updated.
public bool MaterialsChangedInLastUpdate () {
int newSubmeshMaterials = submeshMaterials.Count;
var sharedMaterials = this.sharedMaterials;
@@ -1349,6 +1353,7 @@ namespace Spine.Unity {
return false;
}
+ /// Updates the internal shared materials array with the given instruction list.
public void UpdateSharedMaterials (ExposedList instructions) {
int newSize = instructions.Count;
{ //submeshMaterials.Resize(instructions.Count);
@@ -1405,10 +1410,11 @@ namespace Spine.Unity {
}
}
+ /// Instructions used by a SkeletonRenderer to render a mesh.
public class SkeletonRendererInstruction {
- public bool immutableTriangles;
public readonly ExposedList submeshInstructions = new ExposedList();
+ public bool immutableTriangles;
#if SPINE_TRIANGLECHECK
public bool hasActiveClipping;
public int rawVertexCount = -1;
@@ -1540,4 +1546,5 @@ namespace Spine.Unity {
}
}
+
}
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/AttachmentTools/AttachmentTools.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/AttachmentTools/AttachmentTools.cs
index a6d4a3932..d681b891c 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/AttachmentTools/AttachmentTools.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/AttachmentTools/AttachmentTools.cs
@@ -823,18 +823,12 @@ namespace Spine.Unity.Modules.AttachmentTools {
public static void SetAttachment (this Skin skin, int slotIndex, string keyName, Attachment attachment) {
skin.AddAttachment(slotIndex, keyName, attachment);
}
-
- /// Removes the attachment. Returns true if the element is successfully found and removed; otherwise, false.
- public static bool RemoveAttachment (this Skin skin, string slotName, string keyName, Skeleton skeleton) {
- int slotIndex = skeleton.FindSlotIndex(slotName);
- if (skeleton == null) throw new System.ArgumentNullException("skeleton", "skeleton cannot be null.");
+
+ public static void RemoveAttachment (this Skin skin, string slotName, string keyName, SkeletonData skeletonData) {
+ int slotIndex = skeletonData.FindSlotIndex(slotName);
+ if (skeletonData == null) throw new System.ArgumentNullException("skeletonData", "skeletonData cannot be null.");
if (slotIndex == -1) throw new System.ArgumentException(string.Format("Slot '{0}' does not exist in skeleton.", slotName), "slotName");
- return skin.RemoveAttachment(slotIndex, keyName);
- }
-
- /// Removes the attachment. Returns true if the element is successfully found and removed; otherwise, false.
- public static bool RemoveAttachment (this Skin skin, int slotIndex, string keyName) {
- return skin.Attachments.Remove(new Skin.AttachmentKeyTuple(slotIndex, keyName));
+ skin.RemoveAttachment(slotIndex, keyName);
}
public static void Clear (this Skin skin) {
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonGraphic/SkeletonGraphic.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonGraphic/SkeletonGraphic.cs
index 7d44647f9..49788fbae 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonGraphic/SkeletonGraphic.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonGraphic/SkeletonGraphic.cs
@@ -106,16 +106,21 @@ namespace Spine.Unity {
#endregion
#region Runtime Instantiation
- public static SkeletonGraphic NewSkeletonGraphicGameObject (SkeletonDataAsset skeletonDataAsset, Transform parent) {
- SkeletonGraphic sg = SkeletonGraphic.AddSkeletonGraphicComponent(new GameObject("New Spine GameObject"), skeletonDataAsset);
+ /// Create a new GameObject with a SkeletonGraphic component.
+ /// Material for the canvas renderer to use. Usually, the default SkeletonGraphic material will work.
+ public static SkeletonGraphic NewSkeletonGraphicGameObject (SkeletonDataAsset skeletonDataAsset, Transform parent, Material material) {
+ var sg = SkeletonGraphic.AddSkeletonGraphicComponent(new GameObject("New Spine GameObject"), skeletonDataAsset, material);
if (parent != null) sg.transform.SetParent(parent, false);
return sg;
}
- public static SkeletonGraphic AddSkeletonGraphicComponent (GameObject gameObject, SkeletonDataAsset skeletonDataAsset) {
+ /// Add a SkeletonGraphic component to a GameObject.
+ /// Material for the canvas renderer to use. Usually, the default SkeletonGraphic material will work.
+ public static SkeletonGraphic AddSkeletonGraphicComponent (GameObject gameObject, SkeletonDataAsset skeletonDataAsset, Material material) {
var c = gameObject.AddComponent();
if (skeletonDataAsset != null) {
- c.skeletonDataAsset = skeletonDataAsset;
+ c.material = material;
+ c.skeletonDataAsset = skeletonDataAsset;
c.Initialize(false);
}
return c;
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonRenderSeparator/SkeletonRenderSeparator.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonRenderSeparator/SkeletonRenderSeparator.cs
index 6a3e3d22d..8b8a80ce9 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonRenderSeparator/SkeletonRenderSeparator.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonRenderSeparator/SkeletonRenderSeparator.cs
@@ -37,7 +37,7 @@ using Spine.Unity;
namespace Spine.Unity.Modules {
[ExecuteInEditMode]
- [HelpURL("https://github.com/pharan/spine-unity-docs/blob/master/SkeletonRenderSeparator.md")]
+ [HelpURL("http://esotericsoftware.com/spine-unity-skeletonrenderseparator")]
public class SkeletonRenderSeparator : MonoBehaviour {
public const int DefaultSortingOrderIncrement = 5;
@@ -53,7 +53,8 @@ namespace Spine.Unity.Modules {
#endif
skeletonRenderer = value;
- this.enabled = false; // Disable if nulled.
+ if (value == null)
+ this.enabled = false;
}
}
@@ -105,6 +106,15 @@ namespace Spine.Unity.Modules {
componentRenderers.Add(spr);
}
+ #if UNITY_EDITOR
+ // Make sure editor updates properly in edit mode.
+ if (!Application.isPlaying) {
+ skeletonRenderer.enabled = false;
+ skeletonRenderer.enabled = true;
+ skeletonRenderer.LateUpdate();
+ }
+ #endif
+
return srs;
}
@@ -167,9 +177,7 @@ namespace Spine.Unity.Modules {
skeletonRenderer.GenerateMeshOverride -= HandleRender;
#endif
- #if UNITY_EDITOR
skeletonRenderer.LateUpdate();
- #endif
foreach (var s in partsRenderers)
s.ClearMesh();
@@ -189,7 +197,6 @@ namespace Spine.Unity.Modules {
calculateTangents = skeletonRenderer.calculateTangents,
immutableTriangles = false, // parts cannot do immutable triangles.
pmaVertexColors = skeletonRenderer.pmaVertexColors,
- //renderMeshes = skeletonRenderer.renderMeshes,
tintBlack = skeletonRenderer.tintBlack,
useClipping = true,
zSpacing = skeletonRenderer.zSpacing
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtility.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtility.cs
index 0fab0f8be..28fc69a5b 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtility.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtility.cs
@@ -136,6 +136,11 @@ namespace Spine.Unity {
bool hasConstraints;
bool needToReprocessBones;
+ public void ResubscribeEvents () {
+ OnDisable();
+ OnEnable();
+ }
+
void OnEnable () {
if (skeletonRenderer == null) {
skeletonRenderer = GetComponent();
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtilityBone.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtilityBone.cs
index 172ef16e2..14f24c715 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtilityBone.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtilityBone.cs
@@ -141,23 +141,24 @@ namespace Spine.Unity {
// Use Applied transform values (ax, ay, AppliedRotation, ascale) if world values were modified by constraints.
if (!bone.appliedValid) {
bone.UpdateAppliedTransform();
- if (position)
- thisTransform.localPosition = new Vector3(bone.ax, bone.ay, 0);
+ }
- if (rotation) {
- if (bone.data.transformMode.InheritsRotation()) {
- thisTransform.localRotation = Quaternion.Euler(0, 0, bone.AppliedRotation);
- } else {
- Vector3 euler = skeletonTransform.rotation.eulerAngles;
- thisTransform.rotation = Quaternion.Euler(euler.x, euler.y, euler.z + (bone.WorldRotationX * skeletonFlipRotation));
- }
- }
+ if (position)
+ thisTransform.localPosition = new Vector3(bone.ax, bone.ay, 0);
- if (scale) {
- thisTransform.localScale = new Vector3(bone.ascaleX, bone.ascaleY, 1f);
- incompatibleTransformMode = BoneTransformModeIncompatible(bone);
+ if (rotation) {
+ if (bone.data.transformMode.InheritsRotation()) {
+ thisTransform.localRotation = Quaternion.Euler(0, 0, bone.AppliedRotation);
+ } else {
+ Vector3 euler = skeletonTransform.rotation.eulerAngles;
+ thisTransform.rotation = Quaternion.Euler(euler.x, euler.y, euler.z + (bone.WorldRotationX * skeletonFlipRotation));
}
}
+
+ if (scale) {
+ thisTransform.localScale = new Vector3(bone.ascaleX, bone.ascaleY, 1f);
+ incompatibleTransformMode = BoneTransformModeIncompatible(bone);
+ }
break;
}