Add support for Spine objects (2D skeletal animation) (#5927)

* This allows to use animatable 2D objects created with Spine (purchase a licence on [their website](https://esotericsoftware.com/) if you're interested).
* 2D skeletal animation allows for very smooth animations, and keep resource usage low compared to animations made of frames like Sprite objects. It's perfect for 2D games and can be used for animated characters, avatars, UI elements.
* Many thanks to @f0nar and @LousyMolars for their work on this new feature and associated game examples!

---------

Co-authored-by: Vladyslav Pohorielov <vpohorielov@playtika.com>
Co-authored-by: Gleb Volkov <glebusheg@gmail.com>
Co-authored-by: Florian Rival <Florian.rival@gmail.com>
Co-authored-by: Davy Hélard <davy.helard@gmail.com>
This commit is contained in:
Vladyslav Pohorielov
2024-01-17 19:19:08 +02:00
committed by GitHub
parent f623b352ee
commit d0005ba2cb
82 changed files with 11481 additions and 29854 deletions

2
GDJS/.gitignore vendored
View File

@@ -1 +1 @@
/node_modules
/node_modules

View File

@@ -124,6 +124,8 @@ namespace gdjs {
private _jsonManager: JsonManager;
private _model3DManager: Model3DManager;
private _bitmapFontManager: BitmapFontManager;
private _spineAtlasManager: SpineAtlasManager | null = null;
private _spineManager: SpineManager | null = null;
/**
* Only used by events.
@@ -172,6 +174,18 @@ namespace gdjs {
);
this._model3DManager = new gdjs.Model3DManager(this);
// add spine related managers only if spine extension is used
if (gdjs.SpineAtlasManager && gdjs.SpineManager) {
this._spineAtlasManager = new gdjs.SpineAtlasManager(
this,
this._imageManager
);
this._spineManager = new gdjs.SpineManager(
this,
this._spineAtlasManager
);
}
const resourceManagers: Array<ResourceManager> = [
this._imageManager,
this._soundManager,
@@ -180,6 +194,11 @@ namespace gdjs {
this._bitmapFontManager,
this._model3DManager,
];
if (this._spineAtlasManager)
resourceManagers.push(this._spineAtlasManager);
if (this._spineManager) resourceManagers.push(this._spineManager);
this._resourceManagersMap = new Map<ResourceKind, ResourceManager>();
for (const resourceManager of resourceManagers) {
for (const resourceKind of resourceManager.getResourceKinds()) {
@@ -188,6 +207,13 @@ namespace gdjs {
}
}
/**
* @returns the runtime game instance.
*/
getRuntimeGame(): RuntimeGame {
return this._runtimeGame;
}
/**
* Update the resources data of the game. Useful for hot-reloading, should
* not be used otherwise.
@@ -576,6 +602,24 @@ namespace gdjs {
getModel3DManager(): gdjs.Model3DManager {
return this._model3DManager;
}
/**
* Get the Spine manager of the game, used to load and construct spine skeletons from game
* resources.
* @return The Spine manager for the game
*/
getSpineManager(): gdjs.SpineManager | null {
return this._spineManager;
}
/**
* Get the Spine Atlas manager of the game, used to load atlases from game
* resources.
* @return The Spine Atlas manager for the game
*/
getSpineAtlasManager(): gdjs.SpineAtlasManager | null {
return this._spineAtlasManager;
}
}
type PromiseError<T> = { item: T; error: Error };

View File

@@ -29,8 +29,6 @@ namespace gdjs {
*/
setAnimationName(newAnimationName: string): void;
isCurrentAnimationName(name: string): boolean;
/**
* Return true if animation has ended.
* The animation had ended if:
@@ -117,10 +115,6 @@ namespace gdjs {
this.object.setAnimationName(newAnimationName);
}
isCurrentAnimationName(name: string): boolean {
return this.object.isCurrentAnimationName(name);
}
hasAnimationEnded(): boolean {
return this.object.hasAnimationEnded();
}

View File

@@ -152,6 +152,7 @@ namespace gdjs {
getGlobalResourceNames(data),
data.layouts
);
this._effectsManager = new gdjs.EffectsManager();
this._maxFPS = this._data.properties.maxFPS;
this._minFPS = this._data.properties.minFPS;
@@ -306,6 +307,24 @@ namespace gdjs {
return this._resourcesLoader.getModel3DManager();
}
/**
* Get the Spine manager of the game, used to load and construct spine skeletons from game
* resources.
* @return The Spine manager for the game
*/
getSpineManager(): gdjs.SpineManager | null {
return this._resourcesLoader.getSpineManager();
}
/**
* Get the Spine Atlas manager of the game, used to load atlases from game
* resources.
* @return The Spine Atlas manager for the game
*/
getSpineAtlasManager(): gdjs.SpineAtlasManager | null {
return this._resourcesLoader.getSpineAtlasManager();
}
/**
* Get the input manager of the game, storing mouse, keyboard
* and touches states.
@@ -1152,5 +1171,16 @@ namespace gdjs {
? mapping[embeddedResourceName]
: embeddedResourceName;
}
/**
* Returns the array of resources that are embedded to passed one.
* @param resourceName The name of resource to find embedded resources of.
* @returns The array of related resources names.
*/
getEmbeddedResourcesNames(resourceName: string): string[] {
return this._embeddedResourcesMappings.has(resourceName)
? Object.keys(this._embeddedResourcesMappings.get(resourceName)!)
: [];
}
}
}

View File

@@ -0,0 +1,4 @@
import * as pixi_spine from 'pixi-spine';
export = pixi_spine;
export as namespace pixi_spine;

View File

@@ -280,4 +280,6 @@ declare type ResourceKind =
| 'tilemap'
| 'tileset'
| 'bitmapFont'
| 'model3D';
| 'model3D'
| 'atlas'
| 'spine';

174
GDJS/package-lock.json generated
View File

@@ -19,6 +19,7 @@
"lebab": "^3.1.0",
"minimist": "^1.2.5",
"patch-package": "^6.4.7",
"pixi-spine": "4.0.4",
"pixi.js": "7.3.0",
"prettier": "2.1.2",
"recursive-readdir": "^2.2.2",
@@ -28,6 +29,91 @@
"typescript": "4.3.2"
}
},
"node_modules/@pixi-spine/base": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/base/-/base-4.0.3.tgz",
"integrity": "sha512-0bunaWebaDswLFtYZ6whV+ZvgLQ7oANcvbPmIOoVpS/1pOY3Y/GAnWOFbgp3qt9Q/ntLYqNjGve6xq0IqpsTAA==",
"dev": true,
"peerDependencies": {
"@pixi/core": "^7.0.0",
"@pixi/display": "^7.0.0",
"@pixi/graphics": "^7.0.0",
"@pixi/mesh": "^7.0.0",
"@pixi/mesh-extras": "^7.0.0",
"@pixi/sprite": "^7.0.0"
}
},
"node_modules/@pixi-spine/loader-base": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-base/-/loader-base-4.0.4.tgz",
"integrity": "sha512-Grgu+PxiUpgYWpuMRr3h5jrN3ZTnwyXfu3HuYdFb6mbJTTMub4xBPALeui+O+tw0k9RNRAr99pUzu9Rc9XTbAw==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/assets": " ^7.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi-spine/loader-uni": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-uni/-/loader-uni-4.0.3.tgz",
"integrity": "sha512-tfhTJrnuog8ObKbbiSG1wV/nIUc3O98WfwS6lCmewaupoMIKF0ujg21MCqXUXJvljQJzU9tbURI+DWu4w9dnnA==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi-spine/loader-base": "^4.0.0",
"@pixi-spine/runtime-3.7": "^4.0.0",
"@pixi-spine/runtime-3.8": "^4.0.0",
"@pixi-spine/runtime-4.1": "^4.0.0",
"@pixi/assets": " ^7.0.0",
"@pixi/core": "^7.0.0",
"@pixi/display": "^7.0.0",
"@pixi/graphics": "^7.0.0",
"@pixi/mesh": "^7.0.0",
"@pixi/mesh-extras": "^7.0.0",
"@pixi/sprite": "^7.0.0"
}
},
"node_modules/@pixi-spine/runtime-3.7": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.7/-/runtime-3.7-4.0.3.tgz",
"integrity": "sha512-zuopKtSqjRc37wjW5xJ64j9DbiBB7rkPMFeldeWBPCbfZiCcFcwSZwZnrcgC+f4HIGo0NeviAvJGM8Hcf3AyeA==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi-spine/runtime-3.8": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.8/-/runtime-3.8-4.0.3.tgz",
"integrity": "sha512-lIhb4jOTon+FVYLO9AIgcB6jf9hC+RLEn8PesaDRibDocQ1htVCkEIhCIU3Mc00fuqIby7lMBsINeS/Th0q3bw==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi-spine/runtime-4.0": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.0/-/runtime-4.0-4.0.3.tgz",
"integrity": "sha512-2Y8qhxRkg/yH/9VylGsRVAd5W+dXVPhHTjFk0RR9wEUzTCkdZ17pE+56s2nESi2X3sYNKkz8FowfaqIvXnVGxw==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi-spine/runtime-4.1": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.1/-/runtime-4.1-4.0.3.tgz",
"integrity": "sha512-jK433snCQMC4FUPiDgyIcxhiatvRNSxqgs0CgHjjQ0l8GlY6gPpkkdThQ6GsFNme1SUZ4uvnWwawXFIGjW1IpQ==",
"dev": true,
"peerDependencies": {
"@pixi-spine/base": "^4.0.0",
"@pixi/core": "^7.0.0"
}
},
"node_modules/@pixi/accessibility": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.3.0.tgz",
@@ -1599,6 +1685,30 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pixi-spine": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/pixi-spine/-/pixi-spine-4.0.4.tgz",
"integrity": "sha512-XRq1yARVoi4av7RXnd9+P37SWI9+e4/f5yTScZPJGB+sY5VcRYN6BYkBQ+y8nUKI1aJIjlms9z+pGxqikm+eFQ==",
"dev": true,
"dependencies": {
"@pixi-spine/base": "^4.0.3",
"@pixi-spine/loader-base": "^4.0.4",
"@pixi-spine/loader-uni": "^4.0.3",
"@pixi-spine/runtime-3.7": "^4.0.3",
"@pixi-spine/runtime-3.8": "^4.0.3",
"@pixi-spine/runtime-4.0": "^4.0.3",
"@pixi-spine/runtime-4.1": "^4.0.3"
},
"peerDependencies": {
"@pixi/assets": "^7.0.0",
"@pixi/core": "^7.0.0",
"@pixi/display": "^7.0.0",
"@pixi/graphics": "^7.0.0",
"@pixi/mesh": "^7.0.0",
"@pixi/mesh-extras": "^7.0.0",
"@pixi/sprite": "^7.0.0"
}
},
"node_modules/pixi.js": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.3.0.tgz",
@@ -1981,6 +2091,55 @@
}
},
"dependencies": {
"@pixi-spine/base": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/base/-/base-4.0.3.tgz",
"integrity": "sha512-0bunaWebaDswLFtYZ6whV+ZvgLQ7oANcvbPmIOoVpS/1pOY3Y/GAnWOFbgp3qt9Q/ntLYqNjGve6xq0IqpsTAA==",
"dev": true,
"requires": {}
},
"@pixi-spine/loader-base": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-base/-/loader-base-4.0.4.tgz",
"integrity": "sha512-Grgu+PxiUpgYWpuMRr3h5jrN3ZTnwyXfu3HuYdFb6mbJTTMub4xBPALeui+O+tw0k9RNRAr99pUzu9Rc9XTbAw==",
"dev": true,
"requires": {}
},
"@pixi-spine/loader-uni": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-uni/-/loader-uni-4.0.3.tgz",
"integrity": "sha512-tfhTJrnuog8ObKbbiSG1wV/nIUc3O98WfwS6lCmewaupoMIKF0ujg21MCqXUXJvljQJzU9tbURI+DWu4w9dnnA==",
"dev": true,
"requires": {}
},
"@pixi-spine/runtime-3.7": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.7/-/runtime-3.7-4.0.3.tgz",
"integrity": "sha512-zuopKtSqjRc37wjW5xJ64j9DbiBB7rkPMFeldeWBPCbfZiCcFcwSZwZnrcgC+f4HIGo0NeviAvJGM8Hcf3AyeA==",
"dev": true,
"requires": {}
},
"@pixi-spine/runtime-3.8": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.8/-/runtime-3.8-4.0.3.tgz",
"integrity": "sha512-lIhb4jOTon+FVYLO9AIgcB6jf9hC+RLEn8PesaDRibDocQ1htVCkEIhCIU3Mc00fuqIby7lMBsINeS/Th0q3bw==",
"dev": true,
"requires": {}
},
"@pixi-spine/runtime-4.0": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.0/-/runtime-4.0-4.0.3.tgz",
"integrity": "sha512-2Y8qhxRkg/yH/9VylGsRVAd5W+dXVPhHTjFk0RR9wEUzTCkdZ17pE+56s2nESi2X3sYNKkz8FowfaqIvXnVGxw==",
"dev": true,
"requires": {}
},
"@pixi-spine/runtime-4.1": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.1/-/runtime-4.1-4.0.3.tgz",
"integrity": "sha512-jK433snCQMC4FUPiDgyIcxhiatvRNSxqgs0CgHjjQ0l8GlY6gPpkkdThQ6GsFNme1SUZ4uvnWwawXFIGjW1IpQ==",
"dev": true,
"requires": {}
},
"@pixi/accessibility": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.3.0.tgz",
@@ -3203,6 +3362,21 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"pixi-spine": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/pixi-spine/-/pixi-spine-4.0.4.tgz",
"integrity": "sha512-XRq1yARVoi4av7RXnd9+P37SWI9+e4/f5yTScZPJGB+sY5VcRYN6BYkBQ+y8nUKI1aJIjlms9z+pGxqikm+eFQ==",
"dev": true,
"requires": {
"@pixi-spine/base": "^4.0.3",
"@pixi-spine/loader-base": "^4.0.4",
"@pixi-spine/loader-uni": "^4.0.3",
"@pixi-spine/runtime-3.7": "^4.0.3",
"@pixi-spine/runtime-3.8": "^4.0.3",
"@pixi-spine/runtime-4.0": "^4.0.3",
"@pixi-spine/runtime-4.1": "^4.0.3"
}
},
"pixi.js": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.3.0.tgz",

View File

@@ -16,6 +16,7 @@
"minimist": "^1.2.5",
"patch-package": "^6.4.7",
"pixi.js": "7.3.0",
"pixi-spine": "4.0.4",
"prettier": "2.1.2",
"recursive-readdir": "^2.2.2",
"shelljs": "^0.8.4",
@@ -23,8 +24,19 @@
"typedoc-plugin-reference-excluder": "^1.0.0",
"typescript": "4.3.2"
},
"overrides": {
"pixi-spine": {
"@pixi/assets": "7.3.0",
"@pixi/core": "7.3.0",
"@pixi/display": "7.3.0",
"@pixi/graphics": "7.3.0",
"@pixi/mesh": "7.3.0",
"@pixi/mesh-extras": "7.3.0",
"@pixi/sprite": "7.3.0"
}
},
"scripts": {
"postinstall": "patch-package",
"postinstall": "patch-package && node scripts/install-spine.js",
"check-types": "tsc",
"build": "node scripts/build.js",
"test": "cd tests && npm run test-benchmark",

View File

@@ -0,0 +1,36 @@
const path = require('path');
const shell = require('shelljs');
const readContent = (path, testErrorMessage) => {
if (!shell.test('-f', path)) throw new Error(`${testErrorMessage} Should exist by ${path}.`);
const readingResult = shell.cat(path);
if (readingResult.stderr) throw new Error(readingResult.stderr);
return readingResult.toString();
};
try {
shell.echo(`Start pixi-spine.js copying...`);
const originalSpineDir = path.resolve('node_modules/pixi-spine');
const originalSpinePackage = JSON.parse(readContent(path.join(originalSpineDir, 'package.json'), 'Cannot find pixi-spine package.json file.'));
const originalSpineContent = readContent(path.join(originalSpineDir, originalSpinePackage.extensionConfig.bundle), 'Cannot find pixi-spine.js.');
const varSpineExport = '\nvar pixi_spine = this.PIXI.spine;\n';
const runtimeSpineDir = '../Extensions/Spine/pixi-spine';
if (!shell.test('-d', runtimeSpineDir)) {
shell.echo(`Creating directory for pixi-spine.js ${runtimeSpineDir}.`);
shell.mkdir(runtimeSpineDir);
}
const runtimeSpinePath = path.join(runtimeSpineDir, 'pixi-spine.js');
new shell.ShellString(originalSpineContent + varSpineExport).to(runtimeSpinePath);
shell.echo(`✅ Properly copied pixi-spine.js from node_modules to ${runtimeSpinePath}.`);
} catch(error) {
shell.echo(`❌ Unable to copy pixi-spine.js from node_modules. Error is: ${error}`)
shell.exit(1);
}

View File

@@ -23,6 +23,7 @@ const transformExcludedExtensions = ['.min.js', '.d.ts'];
const untransformedPaths = [
// GDJS prebuilt files:
'GDJS/Runtime/pixi-renderers/pixi.js',
'GDJS/Runtime/pixi-renderers/pixi-spine.js',
'GDJS/Runtime/pixi-renderers/three.js',
'GDJS/Runtime/pixi-renderers/ThreeAddons.js',
'GDJS/Runtime/pixi-renderers/draco/gltf/draco_wasm_wrapper.js',

View File

@@ -0,0 +1,101 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 813, 160, 45, 45
eye-indifferent
bounds: 569, 2, 47, 45
eye-surprised
bounds: 643, 7, 47, 45
rotate: 90
front-bracer
bounds: 811, 51, 29, 40
front-fist-closed
bounds: 807, 93, 38, 41
front-fist-open
bounds: 815, 210, 43, 44
front-foot
bounds: 706, 64, 63, 35
rotate: 90
front-shin
bounds: 80, 11, 41, 92
front-thigh
bounds: 754, 12, 23, 56
front-upper-arm
bounds: 618, 5, 23, 49
goggles
bounds: 214, 20, 131, 83
gun
bounds: 347, 14, 105, 102
rotate: 90
head
bounds: 80, 105, 136, 149
hoverboard-board
bounds: 2, 8, 246, 76
rotate: 90
hoverboard-thruster
bounds: 478, 2, 30, 32
hoverglow-small
bounds: 218, 117, 137, 38
rotate: 90
mouth-grind
bounds: 775, 80, 47, 30
rotate: 90
mouth-oooo
bounds: 779, 31, 47, 30
rotate: 90
mouth-smile
bounds: 783, 207, 47, 30
rotate: 90
muzzle-glow
bounds: 779, 4, 25, 25
muzzle-ring
bounds: 451, 14, 25, 105
muzzle01
bounds: 664, 60, 67, 40
rotate: 90
muzzle02
bounds: 580, 56, 68, 42
rotate: 90
muzzle03
bounds: 478, 36, 83, 53
rotate: 90
muzzle04
bounds: 533, 49, 75, 45
rotate: 90
muzzle05
bounds: 624, 56, 68, 38
rotate: 90
neck
bounds: 806, 8, 18, 21
portal-bg
bounds: 258, 121, 133, 133
portal-flare1
bounds: 690, 2, 56, 30
rotate: 90
portal-flare2
bounds: 510, 3, 57, 31
portal-flare3
bounds: 722, 4, 58, 30
rotate: 90
portal-shade
bounds: 393, 121, 133, 133
portal-streaks1
bounds: 528, 126, 126, 128
portal-streaks2
bounds: 656, 129, 125, 125
rear-bracer
bounds: 826, 13, 28, 36
rear-foot
bounds: 743, 70, 57, 30
rotate: 90
rear-shin
bounds: 174, 14, 38, 89
rear-thigh
bounds: 783, 158, 28, 47
rear-upper-arm
bounds: 783, 136, 20, 44
rotate: 90
torso
bounds: 123, 13, 49, 90

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -41,14 +41,15 @@ module.exports = function (config) {
'./newIDE/app/resources/GDJS/Runtime/AsyncTasksManager.js',
'./newIDE/app/resources/GDJS/Runtime/libs/rbush.js',
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/pixi.js',
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/pixi-spine.js',
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/three.js',
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/*.js',
'./newIDE/app/resources/GDJS/Runtime/howler-sound-manager/howler.min.js',
'./newIDE/app/resources/GDJS/Runtime/howler-sound-manager/howler-sound-manager.js',
'./newIDE/app/resources/GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver.js',
'./newIDE/app/resources/GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver-font-manager.js',
'./newIDE/app/resources/GDJS/Runtime/jsonmanager.js',
'./newIDE/app/resources/GDJS/Runtime/Model3DManager.js',
'./newIDE/app/resources/GDJS/Runtime/jsonmanager.js',
'./newIDE/app/resources/GDJS/Runtime/ResourceLoader.js',
'./newIDE/app/resources/GDJS/Runtime/ResourceCache.js',
'./newIDE/app/resources/GDJS/Runtime/timemanager.js',