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.
Related
I've been searching stack overflow and the internet for a bit trying to search for a solution to this issue, everytime i try to use OnBecameVisible() or OnBecameInvisible() or TestPlanesAABB to check if the object is not visible through the wall, the camera can still see the object through a solid wall.
Video of the problem here:
https://youtu.be/3HiEugm6On8
As you can see, if i'm looking at him he stops moving, if i turn around and he "unloads" or "becomes invisible" he moves closer, but if i go around a corner still looking in his direction he stops moving as if i can see him and there is no wall there, this is what i'm looking to solve
it's an enemy that roams around, and i want him to only move if i cannot see him, which i thought would be fairly simple, but alas it seems not to be as simple as i thought
my current code is basic:
public bool IsSeen = false;
public void OnBecameVisible()
{
Debug.Log("I can now see you");
IsSeen = true;
}
public void OnBecameInvisible()
{
Debug.Log("I can't see you");
IsSeen = false;
}
this is attatched to the object that i wish to detect / not detect through walls, which i do believe checks if the object is viewable by the camera that i choose.
does anyone have any ideas as to how i can fix / achieve this?
As already mentioned the general "problem" with Renderer.OnBecameVisible is
Note that object is considered visible when it needs to be rendered in the Scene. It might not be actually visible by any camera, but still need to be rendered for shadows for example. Also, when running in the editor, the Scene view cameras will also cause this function to be called.
So its not really usable for you.
is there a simple way to raycast the whole screen?
Unfortunately not really :/
You can a bit avoid this using GeometryUtility.CalculateFrustumPlanes in order to get the four planes of the camera frustrum. Then you can check whether the object is actually inside the frustrum using GeometryUtility.TestPlanesAABB
var cam = Camera.main;
var planes = GeometryUtility.CalculateFrustumPlanes(cam);
var objCollider = GetComponent<Collider>();
if (GeometryUtility.TestPlanesAABB(planes, objCollider.bounds))
{
Debug.Log("I am inside the camera frustrum!");
}
else
{
Debug.Log("I am out of sight...");
}
However, this still does not cover any other object being in front of the target object and therefore actually covering it.
You would need to define exactly what visible means (e.g. any portion of the mesh? Is the center of the object enough to test? etc).
For e.g. testing only the center of the object you could use a Physics.Linecast like e.g.
if (GeometryUtility.TestPlanesAABB(planes, objCollider.bounds))
{
Debug.Log("I am inside the camera frustrum!");
if(Physics.LineCast(cam.transform.position, objCollider.GetComponentInChildren<Renderer>().bounds.center, out var hit)
{
if(hit.gameObject != objCollider.gameObject)
{
Debug.Log("..but something else is in the way..");
}
else
{
Debug.Log("Now you see me, muhaha!");
}
}
}
If you want it to be more precise and track of any part of the mesh is visible then it actually gets tricky. You would need to raycast multiple key points of the bounding box (e.g. each corner, centers of the edges etc) depending on your needs.
AppImage I have a wall of 4 rectangles that are different colors, to pass through the wall the color of the ball has to match that of the rectangle on the wall. The ball will pass through the wall, and a new wall will appear. However, when I detect this collision I get multiple collision readings. I have tested this by printing dead or alive, and it prints both or more many times.
func didBegin(_ contact: SKPhysicsContact) {
if let nodeA = contact.bodyA.node as? SKShapeNode, let nodeB = contact.bodyB.node as? SKShapeNode {
if nodeA.fillColor != nodeB.fillColor {
print("DEAD")
}
else {
print("Alive")
}
}
}
please help!!!
Yep - this happens. The way to handle it (you can't get sprite-kit to NOT call didBegin multiple times in some circumstances) is to make sure that your contact code accommodates this and that handling the contract multiple times does not cause a problem (such as adding to the score multiple times, removing multiple lives, trying to access a node or physicsBody that has been removed etc).
There is a discussion here: Sprite-Kit registering multiple collisions for single contact
Some things you can do include:
If you remove a node that is contacted, check for it being nil before
you remove it (for the duplicate contacts)
Add the node to a set and then remove all the nodes in the set in
didFinishUpdate
Add an 'inactive' flag' to the node's userData
Make the node a subclass of SKSpriteNode and add an inactive property
Etc etc.
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.
I'm trying to pool my particle emitter nodes. I re-use them when they are needed by removing them from their old parent node and adding them as a child of a SKSpriteNode at a new location. I leave the emitter node position set to 0,0 so the emitter should appear in the center of its new parent sprite node.
The emitters display correctly the first time they are added as a child to a sprite node, but simply do not show up on subsequent attempts. This all worked great in iOS8 and is only broken in iOS9 (seems like lots of particle emitter bugs in iOS9?)
Here's a basic example of my code when I need to place the particle effect:
if emitter.parent != nil {
emitter.removeFromParent()
}
newLocationSpriteNode.addChild(emitter)
emitter.resetSimulation()
This worked perfectly in iOS8 - I could re-use my emitter nodes at new locations. In iOS9 the nodes only appear the first time this code runs and never show up again after. Do you have any insight into how I might work around this issue? Thanks!
I experienced the exact same problem as you described. Emitters were not visible when re-attached for the second time. Everything worked fine on ios8 though. After several hours of experimenting with different settings I almost gave up.. However, I found a solution that works now. First of all I have a pool of SKEmitterNodes which I re-use during gameplay. This method grabs an emitter from the pool (array) and adds it to the gameplay-layer (SKNode):
func createExplosion(position: CGPoint) {
let emitter = _pool.borrowDirtEmitter()
emitter.resetSimulation() //Important
emitter.position = position
emitter.targetNode = self //Important
if emitter.parent == nil {
self.addChild(emitter)
}
}
So "self" here is the actual node that I attach the emitter to. When the node is offscreen I clean up emitters (and other objects):
if let dirtEmitter = childNode as? SKEmitterNode {
if dirtEmitter.parent != nil {
dirtEmitter.removeFromParent()
}
dirtEmitter.targetNode = nil //Important!
_pool.returnDirtEmitter(dirtEmitter)
}
I haven´t had the time to go into more detail yet, but the "Important" comments should give you some pointers. I will try testing out an approach using an action to remove from parent as well (after x seconds), but since I´m making a side scroller I can get away with cleaning up when emitters are offscreen for now.
Hope this helps..
I am building a game where the player runs on a path. When the player triggers a collider, 2 enemy objects will spawn.
What I want is when the first collider trigger has been entered, I want the second collider, which is at a certain distance from the first collider, to get disabled for a certain time. How to achieve this?
If you'd like to disable the colliders so they won't hit or rebound off the wall, for example, then you can change your collider's "isTrigger" variable to true, to change it into a trigger volume instead of a solid collider. This has the effect of disabling it - in that it won't cause other objects to stop or rebound if they hit it.
For example:
function Update() {
if (Input.GetKeyDown(KeyCode.X)) {
collider.isTrigger = true;
}
}
Note that things like MouseOver still work.
If you want to disable that completely, you can try collider.enabled = false. I'm not sure if that works or not. If it doesn't, you can always scale down your collider:
var myOldSize:Vector3;
function DisableBoxCollider(myCollider:BoxCollider)
{
//actually just resizes it
myOldSize=myCollider.size;
myCollider.size=Vector3(0,0,0);
}
function EnableBoxCollider(myCollider:BoxCollider)
{
if(myOldSize!=Vector3(0,0,0))
myCollider.size=myOldSize;
}
You can use the above code to integrate it in your own project. I'm not going to spill out all of the code for you because else we'd miss the point of learning to program and post on Stackoverflow in general. But it should help you to get on your way. Try and play some with the code, and if you have questions, get back here and ask them, providing the question with some code to show what you have tried.