Is there a way to pool/cache SKReferenceNodes in SpriteKit using Swift?
I am creating a game using xCodes visual level editor. I am creating different .sks files with the visual level editor that I am than calling in code when I need to. I am calling them in code because I am using them to create random levels or obstacles so I don't need all of them added to the scene at once.
At the moment I am doing it like this
I create a convince init method for SKReferenceNodes to init them with URLs. I am doing this because there is some sort of bug calling SKReferenceNodes by file name directly (https://forums.developer.apple.com/thread/44090).
Using such an extension makes makes the code a bit cleaner.
extension SKReferenceNode {
convenience init(roomName: String) {
let path: String
if let validPath = NSBundle.mainBundle().pathForResource(roomName, ofType: "sks") {
path = validPath
} else {
path = NSBundle.mainBundle().pathForResource("RoomTemplate", ofType: "sks")! // use empty roomTemplate as backup so force unwrap
}
self.init(URL: NSURL(fileURLWithPath: path))
}
}
and than in my scenes I can create them and add them like so (about every 10 seconds)
let room = SKReferenceNode(roomName: "Room1") // will cause lag without GCD
room.position = ...
addChild(room)
This works ok but I am getting some lag/stutter when creating these. So I am using GCD to reduce this to basically no stutter. It works well but I am wondering if I can preload all .sks files first.
I tried using arrays to do this but I am getting crashes and it just doesn't seem to work (I also get a message already adding node that has a parent).
I was trying to preload them like so at app launch
let allRooms = [SKReferenceNode]()
for i in 0...3 {
let room = SKReferenceNode(roomName: "Room\(i)")
allRooms.append(room)
}
and than use the array when I need too. This doesn't work however and I am getting a crash when trying to use code like this
let room = allRooms[0]
room.position =
addChild(room) // causes error/crash -> node already has parent
Has anyone done something similar? Is there another way I can pool/cache those reference nodes?. Am i missing something here?
Speaking about the SKReferenceNode preload, I think the policy to be followed is to load your object, find what kind they are and use the official preloading methods available in Sprite-Kit:
SKTextureAtlas.preloadTextureAtlases(_:withCompletionHandler:)
SKTexture.preloadTextures(_:withCompletionHandler:)
To avoid this kind of error you should create separate instances of the nodes.
Try to doing this:
let room = allRooms[0]
room.position = ...
room.removeFromParent()
addChild(room)
I just figured it out, I was just being an idiot.
Using arrays like I wanted to is fine, the problem I had which caused the crash was the following.
When the game scene first loads I am adding 3 rooms but when testing with the arrays I kept adding the same room
let room = allRooms[0]
instead of using a randomiser.
This obviously meant I was adding the same instance multiple times, hence the crash.
With a randomiser, that doesn't repeat the same room, this does not happen.
Furthermore I make sure to remove the room from the scene when I no longer need it. I have a node in the rooms (roomRemover) which fires a method to remove/create a new room once it comes into contact with the player.
This would be the code in DidBeginContact.
guard let roomToRemove = secondBody?.node.parent?.parent?.parent?.parent else { return }
// secondBody is the roomRemover node which is a child of the SKReferenceNode.
// If I want to remove the whole SKReferenceNode I need to add .parent 4 times.
// Not exactly sure why 4 times but it works
for room in allRooms {
if room == roomToRemove {
room.removeFromParent()
}
}
loadRandomRoom()
Hope this helps someone trying to do the same.
Related
So, i am trying to load different Scenes made in RealityComposer, depending on a variable.
What worked so far:
let SceneAnchor = try! Experience1.loadScene()
arView.scene.anchors.append(SceneAnchor)
return arView
Now i looked into apples Documentation and saw the possibility of:
if let anchor = try? Entity.loadAnchor(named: "Scene") {
arView.scene.addAnchor(anchor)
}
where i thought i could just change "Scene" to "Scene(myVar)"
But once i have more than one scene in my file the first solution doesnt work anymore
and the second one doesnt work as well.
What am i missing?
I also looked into working with filenames and was able to make an array of all my .reality Files and Store them in an Array, so i thought i could recall that via the index, but
arrayName[1].loadScene() doesnt seem to work either, eventhough i can print the filenames to console.
Thanks in advance :)
The fact is that Reality Composer creates a separate static method for each scene load. The name of such method is load+scene name. So, if you have 2 scenes in your Exprerience.xcproject with the names Scene and Scene1, then you have 2 static methods
let scene = Experience.loadScene()
let scene1 = Experience.loadScene1()
Unfortunately it is not possible to use the scene name as a parameter, so, you need to use the switch statement in your app to select the appropriate method.
Okay, so I've been struggling around and searching for a while, saw a lot of different posts, but I did not find answer to my question.
My problem:
I have a scene in Unity with nothing in it, everything is created proceduraly and randomly on game start, and of course I want the player to be able to save his progress. I have found ways of saving progress in Unity, but everything was about writing a script for each class or object I want to save, but these seem to me inefficient, since in my game, there are randomly generated houses and buildings (which would be relatively easy to save), but there can also be objects placed inside these buildings and so on. Also later I plan on adding characters, which also need to be saved (like where they are, what are they holding and such). And as I mentioned, writing a save and load script for each object seems inefficient to me, since I'm used to Java's serializtaion, where I just write my Main Object containing all data to a file, so I'm looking for some easier ways to do so. Possibly a way to save entire scene state and then on loading just load the scene instead of generating it from scratch.
My Question:
Is there a way to save whole scene with all objects and information about them and then load it?
Thank you in advance!
There's no built-in method by which you can "save a scene" during runtime and then reload it later. In a Unity build, scenes are stored in a non-editable format, meaning that whenever you load a scene it will load up with the same format as it was built. This is a good thing, because you don't want to edit the contents of your build in a deployed game.
Now, that doesn't mean that a scene can't contain logic to configure itself differently. In fact, it sounds like that's what you're doing. The goal instead is to store the contents into a save file.
Consider switching your mentality from "I want to load a scene that generates random gameplay" to "I want to load a scene that configures itself based on a file." This is a layer of abstraction that gives greater control over what happens when you load your scene.
I would suggest creating a JSON configuration file that stores your important information, something like this might do:
{
"house_locations": [
{
"position": "(0, 0, 0)",
"objects": []
},
{
"position": "(10, 10, 10)",
"objects": []
}
],
"characters": [
{
"position": "(0, 0, 0)",
"inventory": [
{
"item_name": "knife"
},
{
"item_name": "shovel"
}
]
}
]
}
This is just a simple example, as you'll have to add the important data you want to represent your game.
Next, all you have to do when you want to start your game is to do one of the following things:
Are you starting a new game? => Generate a random configuration file, then use that to populate your scene.
Are you loading a saved game? => Use the saved configuration file to populate your scene.
You'll need some kind of WorldBuilder script to handle this. In your scene, you can have something like the following:
public class WorldBuilder : MonoBehaviour
{
// This is the actual contents of the world, represented as a JSON string.
private string _json = "";
public void BuildWorld(string configFilePath)
{
_json = LoadConfiguration(configFilePath);
BuildWorld(_json);
}
public void GenerateWorld()
{
_json = GenerateConfiguration();
BuildWorld(_json);
}
public void SaveWorld(string targetFilePath)
{
// Save the contents of _json out to a file so that it can be loaded
// up again later.
}
private string LoadConfiguration(string configFilePath)
{
// Load the actual file and return the file contents, which is a JSON string.
}
private void BuildWorld(string json)
{
// Actually build the world using the supplied JSON.
}
private string GenerateConfiguration()
{
// Return a randomly generated configuration file.
}
}
This approach separates the problem of saving the contents of the scene and generating the contents of the scene, making the code easier to write and maintain.
I do not know how to build a scene save patternor principles.
But there is a plugin which save scene in play mode, you may configure it according to your project.
https://assetstore.unity.com/packages/tools/utilities/autosaver-don-t-waste-time-anymore-54247
I would name 'LoadConfiguration' to 'LoadWorld'. Actually, I'd name it 'WorldSave' and 'WorldLoad' so it all matches up alphabetically.
Also, have loading be asynchronous:
public IEnumerator WorldLoad(string localWorldFilePath)
{
string filePath = Application.persistentDataPath + "/" + localWorldFilePath + ".json");
string lines = null;
//Loading the level loop:
using (SteamReader streamReader = new StreamReader(filePath,Encoding.UTF8))
{
lines += streamReader.ReadToEnd();
if (UnityEngine.Random.Range(0f,20f) > 15f) //< - probably a fancier way of doing this part)
yield return new WaitForEndOfFrame();
}
//Okay, we're done loading so return this:
yield return lines;
}
In fact, have saving be asynchronous too, using StreamWriter and make sure it's also encoded in UTF8, you can't just use File.Save and File.Load in other words.
Another idea, encrypt your files if you don't want modding (but people will just decrypt it anyway).
Start the coroutine somewhere else, the lower you make that '15f' the more it'll lock up your game, the higher you make it the less it will but the slower it'll go. There's a way to do it on a separate thread or something but I'm not an advanced programmer.
worldBuilder.StartCoroutine(WorldLoad);
TIP 1:
Ideally you wanna create your own scripting language through a console, if the idea of real-time custom scene file loading and saving is that you can create and edit your levels while the actual game is running so that you're bypassing using Unity as a level editor for example.
In other words you could press ~ and bring a console up and type 'WorldLoad(worldNameGoesHere)' and it'll load the world in your games built folders.
There's also a way using an editor script you can actually hook in and override Unity's standard 'Scene Save', in case you wanna do that also.
TIP 2:
Also make sure you encode in UTF8 format like above, saving and loading so you don't have it become a pain later when you want to implement asian languages like Japanese, Chinese, etc...
I'm working on a little project in spritekit and can't quite figure something out. I am animating a sprite using SKAction.animateWithTextures and moving through and array. Works fine just like it should. The issue is that I would like to a function that starts when the animation starts and one when it ends. I see there is a .animationDidStart(CAAnimation), but because what I'm doing is not a CAAnimation I can't really use it. Is there something like this for the method I'm using? As you can may or may not be able to tell I'm still rather new to swift. Thanks for any help in advance.
I'd create a sequence of actions. First a block action to call your method you want called when the animation starts, then your animateWithTextures action, finally another block action that calls your finished method.
let startAction = SKAction.runBlock {
self.startAnimation()
}
let textureAction = SKAction.animateWithTextures...
let finishedAction = SKAction.runBlock {
self.finishedAnimation()
}
SKAction.sequence([startAction, textureAction , finishedAction])
When I run my SpriteKit game, I receive this error multiple times in the console. As far as I can tell (though I'm not completely sure), the game itself is unaffected, but the error might have some other implications, along with crowding the debug console.
I did some research into the error, and found a few possible solutions, none of which seem to have completely worked. These solutions include turning ignoresSiblingOrder to false, and specifying textures as SKTextureAtlas(named: "atlasName").textureNamed("textureName"), but these did not work.
I think the error is coming somewhere from the use of textures and texture atlases in the assets catalogue, though I'm not completely sure. Here is how I am implementing some of these textures/images:
let Texture = SKTextureAtlas(named: "character").textureNamed("\character1")
character = SKSpriteNode(texture: Texture)
also:
let Atlas = SKTextureAtlas(named: "character")
var Frames = [SKTexture]()
let numImages = Atlas.textureNames.count
for var i=1; i<=numImages; i++ {
let textureName = "character(i)"
Frames.append(Atlas.textureNamed(textureName))
}
for var i=numImages; i>=1; i-- {
let TextureName = "character(i)"
Frames.append(Atlas.textureNamed(textureName))
}
let firstFrame = Frames[0]
character = SKSpriteNode(texture: firstFrame)
The above code is just used to create an array from which to animate the character, and the animation runs completely fine.
For all my other sprite nodes, I initialize with SKSpriteNode(imageNamed: "imageName") with the image name from the asset catalogue, but not within a texture atlas. All the images have #1x, #2x, and #3x versions.
I'm not sure if there are any other possible sources for the error message, or if the examples above are the sources of the error.
Is this just a bug with sprite kit, or a legitimate error with my code or assets?
Thanks!
I have this error too. In my opinion, it's the Xcode 7.2 bug and not your fault. I've updated Xcode in the middle of making an app and this message starts to show up constantly in the console. According to this and that links, you have nothing to fear here.
Product > Clean
seems to do the trick.
Error seems to start popping up when you delete an Item from Asset Catalogue but its reference still stay buried in code somewhere. (In my case it was the default spaceship asset which I deleted.)
I have a 3D scene created in scene kit comprising an area surrounded by invisible walls and I want to lob physics objects into this area in such a way that they can't escape once they're in. I had a mind to achieve this in the following fashion:
Create a wall object
Create a 'solidifier' that fits neatly inside the walls
Set each object's isInScene variable to false
Lob them in the vague direction of the solidifier
Upon each update, if an object is touching the solidifier but is not touching the walls, I change its collision mask to include the walls and set isInScene to true so I don't have to check it again.
This often seems to work very well, but every so often (and sadly quite often) I get an EXC_BAD_ACCESS error out of nowhere. The offending method seems to be contactTestBetweenBody, which I am using to determine when an object is touching either the walls or solidifier at times when normal collision detection is turned off. This is necessary to prevent the objects simply bouncing off the outside of the wall object.
Here's a small snippet of code to illustrate. Incidentally, 'objects' is a structure that retains a reference to the node along with other useful details:
if let solid = solidifier?.physicsBody, let wall = walls?.physicsBody {
let world = scene.physicsWorld
for i in 0 ..< objects.count {
if objects[i].isInScene == false, let body = objects[i].node.physicsBody {
let contactSolidifier = world.contactTestBetweenBody(body, andBody: solid, options: nil)
if contactSolidifier != nil {
let contactWall = world.contactTestBetweenBody(body, andBody: wall, options: nil)
if contactWall == nil {
objects[i].isInScene = true
body.collisionBitMask = CollisionMask.allSolids.rawValue
}
}
}
}
}
I found a much, much better solution to the problem that I just plain didn't think of for some reason. Forget about this ridiculously convoluted means of keeping the objects in view. Instead, just make the area you want to retain the objects within and reverse its facet direction and normals.
What I didn't realise was that SceneKit uses backface culling on collision detection too, so if a physics object hits the OUTSIDE of an object that is inside out it will pass clean through.
I would still be interested to know the reason for the bad access error still though, as the contact test methods are useful and I may want to use them for other reasons in the future.