Bevy - Have multiple Sprites Sheets per Entity - bevy

I have a Entity containing a Srite Sheet and class instance
let texture_handle = asset_server.load("turret_idle.png");
let texture_atlas: TextureAtlas = TextureAtlas::from_grid(texture_handle, ...);
let texture_atlas_handle = texture_atlases.add(texture_atlas);
let mut turret = Turret::create(...);
commands.spawn_bundle(SpriteSheetBundle {
texture_atlas: texture_atlas_handle,
transform: Transform::from_xyz(pos),
..default()
})
.insert(turret)
.insert(AnimationTimer(Timer::from_seconds(0.04, true)));
The AnimationTimer will then be used in a query together with the Texture Atlas Handle to render the next sprite
fn animate_turret(
time: Res<Time>,
texture_atlases: Res<Assets<TextureAtlas>>,
mut query: Query<(
&mut AnimationTimer,
&mut TextureAtlasSprite,
&Handle<TextureAtlas>,
)>,
) {
for (mut timer, mut sprite, texture_atlas_handle) in &mut query {
timer.tick(time.delta());
if timer.just_finished() {
let texture_atlas = texture_atlases.get(texture_atlas_handle).unwrap();
sprite.index = (sprite.index + 1) % texture_atlas.textures.len();
}
}
}
This works perfectly fine as long as the tower is idle thus plays the idle animation. As soon as a target is found and attacked, I want to display another sprite sheet instead.
let texture_handle_attack = asset_server.load("turret_attack.png");
Unfortunately, It seems that I cannot add multiple TextureAtlas Handles to a Sprite Sheet Bundle and decide later which one to render. How do I solve this? I thought about merging all animations into one Sprite Sheet already but this is very messy as they have different frames.

Maybe create a struct with all the different handles you need and add it as a resource? Then you need a component for the enum states "idle", "attacked" etc.. and a system that handles setting the correct handle in texture_atlas from your resource handles.

Related

How do I load my own Reality Composer scene into RealityKit?

I have created 3 "scenes" inside Experience.rcproject file, that is created when you start a new Augmented Reality project using xcode.
Working for 3D a lot, I would say that these were 3 objects inside a scene, but inside Experience.rcproject I have added 3 "scenes". Inside each one, the same 3D model. The first one is attached to an horizontal plane, the second one to a vertical plane and the third one to an image.
I am woking with Reality Kit for the first time and learning along the way.
My idea of doing so, is to load the right object when I want to have it attached to the horizontal, vertical or image.
This is how I accomplished this.
I have modified Experience.swift file provided by Apple to accept scene names, like this:
public static func loadBox(namedFile:String) throws -> Experience.Box {
guard let realityFileURL = Foundation.Bundle(for: Experience.Box.self).url(forResource: "Experience", withExtension: "reality") else {
throw Experience.LoadRealityFileError.fileNotFound("Experience.reality")
}
let realityFileSceneURL = realityFileURL.appendingPathComponent(namedFile, isDirectory: false)
let anchorEntity = try Experience.Box.loadAnchor(contentsOf: realityFileSceneURL)
return createBox(from: anchorEntity)
}
and I call this line
let entity = try! Experience.loadBox(namedFile:sceneName)
whatever I want, but I have to use this code:
// I have to keep a reference to the entity so I can remove it from its parent and nil
currentEntity?.removeFromParent()
currentEntity = nil
// I have to load the entity again, now with another name
let entity = try! Experience.loadBox(namedFile:sceneName)
// store a reference to it, so I can remove it in the future
currentEntity = entity
// remove the old one from the scene
arView.scene.anchors.removeAll()
// add the new one
arView.scene.anchors.append(entity)
This code is stupid and I am sure there is a better way.
Any thoughts?
Hierarchy in RealityKit / Reality Composer
I think it's rather a "theoretical" question than practical. At first I should say that editing Experience file containing scenes with anchors and entities isn't good idea.
In RealityKit and Reality Composer there's quite definite hierarchy in case you created single object in default scene:
Scene –> AnchorEntity -> ModelEntity
|
Physics
|
Animation
|
Audio
If you placed two 3D models in a scene they share the same anchor:
Scene –> AnchorEntity – – – -> – – – – – – – – ->
| |
ModelEntity01 ModelEntity02
| |
Physics Physics
| |
Animation Animation
| |
Audio Audio
AnchorEntity in RealityKit defines what properties of World Tracking config are running in current ARSession: horizontal/vertical plane detection and/or image detection, and/or body detection, etc.
Let's look at those parameters:
AnchorEntity(.plane(.horizontal, classification: .floor, minimumBounds: [1, 1]))
AnchorEntity(.plane(.vertical, classification: .wall, minimumBounds: [0.5, 0.5]))
AnchorEntity(.image(group: "Group", name: "model"))
Here you can read about Entity-Component-System paradigm.
Combining two scenes coming from Reality Composer
For this post I've prepared two scenes in Reality Composer – first scene (ConeAndBox) with a horizontal plane detection and a second scene (Sphere) with a vertical plane detection. If you combine these scenes in RealityKit into one bigger scene, you'll get two types of plane detection – horizontal and vertical.
Two cone and box are pinned to one anchor in this scene.
In RealityKit I can combine these scenes into one scene.
// Plane Detection with a Horizontal anchor
let coneAndBoxAnchor = try! Experience.loadConeAndBox()
coneAndBoxAnchor.children[0].anchor?.scale = [7, 7, 7]
coneAndBoxAnchor.goldenCone!.position.y = -0.1 //.children[0].children[0].children[0]
arView.scene.anchors.append(coneAndBoxAnchor)
coneAndBoxAnchor.name = "mySCENE"
coneAndBoxAnchor.children[0].name = "myANCHOR"
coneAndBoxAnchor.children[0].children[0].name = "myENTITIES"
print(coneAndBoxAnchor)
// Plane Detection with a Vertical anchor
let sphereAnchor = try! Experience.loadSphere()
sphereAnchor.steelSphere!.scale = [7, 7, 7]
arView.scene.anchors.append(sphereAnchor)
print(sphereAnchor)
In Xcode's console you can see ConeAndBox scene hierarchy with names given in RealityKit:
And you can see Sphere scene hierarchy with no names given:
And it's important to note that our combined scene now contains two scenes in an array. Use the following command to print this array:
print(arView.scene.anchors)
It prints:
[ 'mySCENE' : ConeAndBox, '' : Sphere ]
You can reassign a type of tracking via AnchoringComponent (instead of plane detection you can assign an image detection):
coneAndBoxAnchor.children[0].anchor!.anchoring = AnchoringComponent(.image(group: "AR Resources",
name: "planets"))
Retrieving entities and connecting them to new AnchorEntity
For decomposing/reassembling an hierarchical structure of your scene, you need to retrieve all entities and pin them to a single anchor. Take into consideration – tracking one anchor is less intensive task than tracking several ones. And one anchor is much more stable – in terms of the relative positions of scene models – than, for instance, 20 anchors.
let coneEntity = coneAndBoxAnchor.goldenCone!
coneEntity.position.x = -0.2
let boxEntity = coneAndBoxAnchor.plasticBox!
boxEntity.position.x = 0.01
let sphereEntity = sphereAnchor.steelSphere!
sphereEntity.position.x = 0.2
let anchor = AnchorEntity(.image(group: "AR Resources", name: "planets")
anchor.addChild(coneEntity)
anchor.addChild(boxEntity)
anchor.addChild(sphereEntity)
arView.scene.anchors.append(anchor)
Useful links
Now you have a deeper understanding of how to construct scenes and retrieve entities from those scenes. If you need other examples look at THIS POST and THIS POST.
P.S.
Additional code showing how to upload scenes from ExperienceX.rcproject:
import ARKit
import RealityKit
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
override func viewDidLoad() {
super.viewDidLoad()
// RC generated "loadGround()" method automatically
let groundArrowAnchor = try! ExperienceX.loadGround()
groundArrowAnchor.arrowFloor!.scale = [2,2,2]
arView.scene.anchors.append(groundArrowAnchor)
print(groundArrowAnchor)
}
}

Occasional stuttering with SceneKit when touching the screen

When I interact with the screen the objects in my game start to stutter. My FPS is at 60 and doesn't drop but the stuttering is still prevalent. I believe my problem is how I'm animating the objects on screen(code below).If anybody could help I would appreciate it.
I have an x amount of nodes inside an array called _activePool. In the Update function I am moving the nodes x position inside _activePool, adding new nodes when the last node in _activePool position is <= 25 and removing the first node in _activePool if it's position is <= -25.
if _cycleIsActive{
for obj in _activePool{
//move the obj in _activePool
obj.position.x += Float(dt * self.speedConstant);
}
let lastObj = _activePool.last;
if (lastObj?.position.x)! + getWidthOfNode(node: lastObj!) + Float(random(min: 15, max: 20)) <= 25{
// get new obj(pattern) and add to _activePool
self.getPatternData(sequencePassedIn: selectedSeq, level: self._currentLevel, randomPattern: randomPattern());
}
let firstObj = _activePool.first;
if (firstObj?.position.x)! + getWidthOfNode(node: firstObj!) <= -25{
// remove object and return to specific pool
firstObj?.removeFromParentNode();
returnItems(item: firstObj!);
_activePool.removeFirst();
}
}
I create several arrays and add them to a dictionary
func activatePools(){
temp1Pool = ObjectPool(tag: 1, data: []);
dictPool[(temp1Pool?.tag)!] = temp1Pool;
temp2Pool = ObjectPool(tag: 2, data: []);
dictPool[(temp2Pool?.tag)!] = temp2Pool;
for i in 0... dictPool.count {
obstacleCreationFactory(factorySwitch: i);
}
}
Creating my obstacles(enemies)
func obstacleCreationFactory(factorySwitch: Int){
Enemies = Enemy();
switch factorySwitch {
case 0:
for _ in 0...100{
let blueEnemy = Enemies?.makeCopy() as! Enemy
blueEnemy.geometry = (Enemies?.geometry?.copy() as! SCNGeometry);
blueEnemy.geometry?.firstMaterial?.diffuse.contents = UIColor.blue;
blueEnemy.tag = 1;
temp1Pool?.addItemToPool(item: blueEnemy);
}
case 1:
for _ in 0...100{
let redEnemy = Enemies?.makeCopy() as! Enemy
redEnemy.geometry = (Enemies?.geometry?.copy() as! SCNGeometry);
redEnemy.geometry?.firstMaterial?.diffuse.contents = UIColor.red;
redEnemy.tag = 2;
temp2Pool?.addItemToPool(item: redEnemy);
}
default:
print("factory error");
}
}
Without being able to look at the rest of your code base it’s really difficult to guess what would be causing your issue.
If somewhere you are creating a ton of temporary objects in a loop somewhere, you might consider creating a local autorelease pool to prevent memory spikes. Here is a good article that describes why in some situations it’s a good idea.
You could also be calling some particularly expensive functions on a timer or something. It’s difficult to say.
In short, you should consider using Xcode’s Profiling tools (called Instruments). Specifically I would recommend using Time Profiler to examine what functions are taking the most time and causing those spikes.
Here is a great WWDC session video that shows how you can use the time profiler, I’d recommend regularly profiling your app, especially when you have an issue like this.

How to execute multiple custom animation functions synchronously in Swift 4

I am working on an IOS App that is supposed to help people in a tourist region to make sense of means of transport. To make it crystal clear to them how to get from A to B, routes are animated with Annotation objects. For example once the user chooses to see the route from A to D, a cable car object slides from A to B. Once it finishes, a Shuttle Bus object moves along a road from B to C, followed by a boat sliding from C to D.
So I wrote the following functions.
This one lets a transportMode object (small image of a boat, cable car, etc.) slide in a straight line from A to B or B to A.
func animateLinearRoute(transportMode: TransportAnnot, startcoor:
CLLocationCoordinate2D, destcoor: CLLocationCoordinate2D){
UIView.animate(withDuration: 3, animations:
{
if (transportMode.coordinate.latitude == startcoor.latitude && transportMode.coordinate.longitude == startcoor.longitude) {
transportMode.coordinate = destcoor
} else {
transportMode.coordinate = startcoor
}
})
}
For moving an object along a nonlinear route (usually a road) drawn on a map I use the following function:
// pass mode of transport and coordinates along the travel route
func animateNonLinearRoute(transportMode: TransportAnnot, animroute: [CLLocationCoordinate2D]){
let path = UIBezierPath()
// get start point of route from coordinates and start drawing route
let point = self.mapView.convert(animroute[0], toPointTo: self.mapView)
path.move(to: point)
// translate each coordinate along the route into a point in the view for drawing
for coor in animroute {
let point = self.mapView.convert(coor, toPointTo: self.mapView)
path.addLine(to: point)
}
// create keyframe animation to move annotation along the previously drawn path
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path.cgPath
animation.duration = 5.0
animation.isRemovedOnCompletion = false
let transportview = self.mapView.view(for: transportMode)
transportview?.layer.add(animation, forKey: "animate position along path")
transportMode.coordinate = animroute[animroute.count - 1]
CATransaction.commit()
}
Now the full route can consist of an arbitrary chain of these methods. For example the user may chose to get to a point that requires a linear route -> nonlinear route -> linear route -> nonlinear -> nonlinear.
Ultimately the animations need to be executed in a strictly consecutive manner so the user won't be confused (the second animation should not start unless the first one has finished, etc.).
One consideration would be a keyframe animation like this:
UIView.animateKeyframes(withDuration: 8, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 5/8, animations: {
self.animateNonLinearRoute(transportMode: self.bus, animroute: self.br.coordinates)
})
UIView.addKeyframe(withRelativeStartTime: 5/8, relativeDuration: 3/8, animations: {
self.animateLinearRoute(transportMode: self.cableCar, startcoor: self.lowerstation, destcoor: self.midstation)
})
// dynamically fill up as needed using appropriate relative start times and durations
}, completion: nil)
That doesn't execute the code synchronously though. I guess it conflicts with the timings and keyframes defined within the functions.
I've been messing around with custom completion closures and then put each method in the completion closure of the previous one as well as with dispatch queues. But I don't really seem to understand them because I wasn't able to achieve the desired effects. And as routes get longer nested completion closures don't seem to be an ideal option as they make the program unnecessarily complex. Any suggestions are highly appreciated.
I guess I traced down the problem.
Since animateNonLinearRoute triggers an animation on the CALayer, the function returns after triggering the animation without waiting for its completion. Therefore all functions in the methods completion handler will be executed, after the CALayer Animation HAS BEEN TRIGGERED, NOT AFTER IT HAS FINISHED.
A simple hacky solution would be to wrap the functions that should be executed after the CALayer Animations have finished, into a CATransaction block:
CATransaction.begin()
CATransaction.setCompletionBlock({
self.animateLinearRoute(transportMode: self.cableCar, startcoor: self.lowerstation, destcoor: self.midstation)
})
self.animateNonLinearRoute(transportMode: self.bus, animroute: self.br.coordinates)
CATransaction.commit()
I would love to hear a better explanation from someone who understands multithreading and concurrency in Swift and a suggestion for a clean way to chain several of these function calls.

SKAction Sequencing and Grouping Animations

I'm doing some death animations for a game, and wanted to ask for some help. I want my monster to disappear in a puff of smoke, but not before it animates a slash effect going across his body.
I have 3 animations that I want to use:
weaponSlash - a line that draws across the monster. Looks like you slashed him with a sword.
smoke - a puff of smoke that slowly expands out
monsterFalling - the monster falls back, startled
What I want to do is play it in this order:
Simultaneously, the slash appears & the monster starts to fall back
About 0.25s into the above animation, I want the cloud to start to appear
When the cloud is about to end (so maybe after 1s) I want the monster to disappear
Remove the smoke, the monster, the sword, etc, and drop some coins on the ground
I started like this, as a test that works somewhat: (ignore the above times for now)
//Cancel any current actions, like a monster attacking
monster.removeAllActions()
//since you can't play 3 animations on one node at the same time, you have to create 3 separate nodes for everything
let slash = SKSpriteNode()
let cloud = SKSpriteNode()
cloud.size = monster.size
slash.size = monster.size
monster.addChild(cloud)
monster.addChild(slash)
//Start the slash animation
slash.run(self.player.currentlyEquippedWeapon.attackAnimation())
//Start the cloud animation (how I get it is elsewhere and not relevant)
cloud.run(cloudAnimation)
//Run the monster death animation, followed by the cleanup/coin dropping
monster.run(SKAction.sequence([monster.deathAnimation(), SKAction.wait(forDuration: 1), postDeathActions]))
The variable PostDeathActions above simply removes the monster node and animates some coins falling.
WHERE I NEED SOME HELP
So the above code doesn't work so great in that the animations all run independently of each other. Based on this, you can see how regardless of whether the slash/cloud finish, the monster will run two actions: him falling back, followed by cleanup, which just removes the monster and spawns the coins. As you can see I tried to delay this by adding a 1s delay but this is all somewhat of a hack since I may have different monsters or attacks, etc, that are faster/slower. I'd rather guarantee that everything finishes before I despawn the monster.
I tried to group this into an SKAction.Run like so:
let preDeath = SKAction.run {
[unowned self] in
monster.run(monster.deathAnimation()
slash.run(self.player.currentlyEquippedWeapon.attackAnimation())
cloud.run(cloudAnimation)
}
but this runs everything at the same time again.
What I want to do is sequence it like this (pseudo code):
let preDeathAnimations = SKAction.Group([slash, cloud, monsterDeathAnimation])
])
SKAction.sequence([preDeathAnimations, postDeathActions])
So this way it'll run all 3 before running cleanup.
Is there a way to do something like this? I know Sequnce/Group need to be run against an SKNode, but I don't have 3 separate ones.
Thanks for your time reading this and any advice you can offer!
This is one idea that I had, but you could use threading + state + onCompletion blocks to take the math out of it. I didn't test it out fully but this general concept should work:
let slash = SKAction.fadeIn(withDuration: 0.5)
let fall = SKAction.fadeOut(withDuration: 0.25)
let puff = SKAction.fadeIn(withDuration: 0.1)
// Put in ALL of the actions from ALL parties that you want to happen prior to puff:
func findLongestTime(from actions: [SKAction]) -> TimeInterval {
var longestTime = TimeInterval(0)
for action in actions {
if action.duration > longestTime { longestTime = action.duration }
}
// Note, if you put a sequence into this function I don't know if it will work right..
// Might need another func like `findDurationOfSequence(_ sequence: SKAction) -> TimeInterval
return longestTime
}
// Note, if you have the monster doing more than falling prior to puff, then you will
// need to subtract those as well:
let monsterActionsPriorToPuff = [fall]
// Add the duration of all monster animations prior to puff:
var MAPTP_duration = TimeInterval(0)
for action in monsterActionsPriorToPuff {
MAPTP_duration += action.duration
}
// Calculate our final wait time, with no negative numbers:
var waitTime = findLongestTime(from: [slash, fall]) - MAPTP_duration
if waitTime < 0 { waitTime = 0 }
let wait = SKAction.wait(forDuration: waitTime)
// Our monster sequence (I forgot to add the disappear, just add after puff)
let monsterSequence = SKAction.sequence([fall, wait, puff])
// Player slashes:
SKSpriteNode().run(slash)
// Monster will wait 0.25 seconds after falling,
// for slash to finish before puffing:
SKSpriteNode().run(monsterSequence)
et me know if this idea isn't working I can try updating it.

Adding and transitioning animations in SceneKit

I looked at Banana game from WWDC which is written in Objective-C was trying to convert the code to Swift for importing animation and transitioning between them but I am having problems running the animations in swift from DAE files.
I have exporter DAE files in both AutoDesk format from 3dsMax and in openCollada format. The Autodesk format the animation is for each bone so I am unable to call an animation by name so I just import the scene and do the following for the animation to start as soon as the file loads.
scene = SCNScene(named: "monster.scnassets/monsterScene.DAE")
scene2 = SCNScene(named:"monster.scnassets/monster.DAE")
var heroNode = SCNNode()
heroNode = scene.rootNode.childNodeWithName("heroNode", recursively: false)!
var nodeArray = scene2.rootNode.childNodes
for childNode in nodeArray {
heroNode.addChildNode(childNode as SCNNode)
}
Although the animation plays as soon as the scene starts I done know how to store the animation.
If I export the collada file using openCollada. I can use the following to run the get and run the animation as there is just one animation for the whole object instead of each bone in case of AutoDesk collada format. By this way I can store the animation also using CAAnimation.
var anim = scene2.rootNode.animationForKey("monster-1")
childNode.addAnimation(anim, forKey: "monster-1")
But then the character runs at an angle and also runs back and forth instead of running at the same spot.
Also the lighting is better using openCollada. I just would like to use openCollada instead of autodesk collada export. Right now I am using openCollada format for exporting the scene and autodesk for exporting character.
How do store animations in SceneKit/Swift and transition between them? Thanks.
If you don't change the default options when loading a scene from a file, all animations in the scene immediately and automatically attach to their target nodes and play. (See the Animation Import Options in the SceneKit API reference.)
If you want to load the animations from a scene file and hold on to them for attaching to nodes (that is, playing) later, you're better off loading them with the SCNSceneSource class. In addition, you can (but don't have to) store your base model in one file and animations in other files.
Just look at this Bananas animation loading code. Just look at it.*
// In AAPLGameLevel.m:
SCNNode *monkeyNode = [AAPLGameSimulation loadNodeWithName:nil fromSceneNamed:#"art.scnassets/characters/monkey/monkey_skinned.dae"];
AAPLMonkeyCharacter *monkey = [[AAPLMonkeyCharacter alloc] initWithNode:monkeyNode];
[monkey createAnimations];
// In AAPLSkinnedCharacter.m (parent class of AAPLMonkeyCharacter):
+ (CAAnimation *)loadAnimationNamed:(NSString *)animationName fromSceneNamed:(NSString *)sceneName
{
NSURL *url = [[NSBundle mainBundle] URLForResource:sceneName withExtension:#"dae"];
SCNSceneSource *sceneSource = [SCNSceneSource sceneSourceWithURL:url options:nil ];
CAAnimation *animation = [sceneSource entryWithIdentifier:animationName withClass:[CAAnimation class]];
//...
}
// In AAPLMonkeyCharacter.m:
- (void)update:(NSTimeInterval)deltaTime
{
// bunch of stuff to decide whether/when to play animation, then...
[self.mainSkeleton addAnimation:[self cachedAnimationForKey:#"monkey_get_coconut-1"] forKey:nil];
//...
}
What's going on here:
There's a custom class managing the animated character. It owns a SCNNode containing the character model, as well as a bunch of CAAnimations for all of the things the model can do (idle/jump/throw/etc).
That class is initialized by passing the character node loaded from one DAE file. (AAPLGameSimulation loadNodeWithName:fromSceneNamed: is a convenience wrapper around loading a SCNScene from a file and grabbing a named node out of it.) That DAE file contains only the character model, with no animations.
Then, AAPLMonkeyCharacter loads (and stores references to) the animations it needs from the separate DAE files containing each animation. This is where SCNSceneSource comes in — it lets you grab animations out of the file without playing them.
When it's time to play, the monkey class calls addAnimation:forKey: to run the animation on its main node.
Translating to Swift and applying to your problem — where you seem to have all animations in the same file — I'd do something like this (vague outline of a hypothetical class):
class Monster {
let node: SCNNode
let attackAnimation: CAAnimation
init() {
let url = NSBundle.mainBundle().URLForResource(/* dae file */)
let sceneSource = SCNSceneSource(URL: url, options: [
SCNSceneSourceAnimationImportPolicyKey : SCNSceneSourceAnimationImportPolicyDoNotPlay
])
node = sceneSource.entryWithIdentifier("monster", withClass: SCNNode.self)
attackAnimation = sceneSource.entryWithIdentifier("monsterIdle", withClass: CAAnimation.self)
}
func playAttackAnimation() {
node.addAnimation(attackAnimation, forKey: "attack")
}
}
The key bits:
SCNSceneSourceAnimationImportPolicyDoNotPlay makes sure that nodes loaded from the scene source don't start with animations attached/playing.
You have to load the animations separately with entryWithIdentifier:withClass:. Be sure to configure them how you like (repeating, fade duration etc) before attaching to the nodes.
2022 code fragment.
In terms of the swift sample in #rickster 's answer.
It seems the code is now more like:
let p = Bundle.main.url(forResource: "File Name", withExtension: "dae")!
source = SCNSceneSource(url: p, options: nil)!
let geom = source.entryWithIdentifier("geometry316",
withClass: SCNGeometry.self)!
as SCNGeometry
yourDragonNode = SCNNode(geometry: geom)
yourAnime = source.entryWithIdentifier("unnamed_animation__0",
withClass: CAAnimation.self)!
In terms of how to get the "mystery strings":
"geometry316" and "unnamed_animation__0" in the example.
notice: https://stackoverflow.com/a/75088130/294884
and: https://stackoverflow.com/a/56787980/294884