I am working on a project with 20 sprites and I am noticing a lot of code repetition. My sprites are named: sprite1, sprite2, sprite3.... so I am looking for a way to reference a sprite based on 2 variables. The first simply being "sprite" and the second being the integer, so I can change or loop through a range at any time. Is it possible to do something like this?
Random example:
x = 1
while x <= 10 {
sprite & x.physicsBody.affectedByGravity = true
x++
}
instead of:
sprite1.physicsBody.affectedByGravity = true
sprite2.physicsBody.affectedByGravity = true
sprite3.physicsBody.affectedByGravity = true
sprite4.physicsBody.affectedByGravity = true
sprite5.physicsBody.affectedByGravity = true
so on...
This could be as simple as:
for sprite in [sprite1, sprite2, sprite3, /* ... */] {
sprite.physicsBody.affectedByGravity = true
}
However, having a bunch of local variables (or instance variables) like sprite1, sprite2, etc suggests that you're not making good use of your tools in some other way. What's meaningful about these sprites? Is there a way you can use the scene structure to better manage them?
For example, if all of the nodes you want to turn gravity on for are children of the same parent node, you could instead do something like:
for sprite in someContainerNode.children {
sprite.physicsBody.affectedByGravity = true
}
Or, if there's some subset of nodes in your scene that you need to do this for, you could give them all the same name (say, in the SpriteKit Scene Editor in Xcode) and use that to find them:
scene.enumerateChildNodesWithName("needsGravity") { node, stop in
node.physicsBody.affectedByGravity = true
}
Related
My game is going to be using touch controls and I'm having a lot of trouble implementing the following mechanics.
When I make a short click on a sprite, I want it to do a simple click event, in my code, it should highlight the sprite.
But when I make a long click on a sprite, it should at one point anchor to the cursor and be dragged around by it. When I release it, it should then stop following the cursor and stay in place.
Last but not least, I'm actually working with multiple sprites and I want to always influence the top-most sprite (z_index wise).
So right now, my code looks like this:
# Node2D, parent of my sprite
func _process(delta):
# Checks for clicks and hold, calls function based on events
if not owning_hand or not owning_hand.my_hand:
is_holding = false
return
if Input.is_action_just_released("click"):
owning_hand.drop_card()
is_holding = false
if not input_in_sprite:
is_holding = false
return
if Input.is_action_just_pressed("click"):
is_holding = true
if Input.is_action_just_released("click"):
if hold_time < HOLD_TARGET:
owning_hand.clicked_cards.append(self)
is_holding = false
if is_holding:
hold_time += delta
else:
hold_time = 0
if hold_time >= HOLD_TARGET:
owning_hand.held_cards.append(self)
func _input(event):
# If the mouse is in the sprite, set input_in_sprite to true
if not owning_hand or not owning_hand.my_hand:
return
if event is InputEventMouse and sprite.get_rect().has_point(to_local(event.position)) and not played:
input_in_sprite = true
else:
input_in_sprite = false
# Position2D, represents the player's input and methods
func _process(delta): # Gets the top most card z_index wise
# Checks for any clicked cards
print(held_cards)
if dragged_card:
dragged_card.position = get_global_mouse_position()
if clicked_cards:
var top_card = clicked_cards[0]
for Card in clicked_cards:
if Card.z_index > top_card.z_index:
top_card = Card
clicked_cards.clear()
highlight_card(top_card)
if held_cards:
var top_card = held_cards[0]
for Card in held_cards:
if Card.z_index > top_card.z_index:
top_card = Card
held_cards.clear()
drag_card(top_card)
func drag_card(card):
# Drags the card around
dragged_card = card
func drop_card():
# Drops the card
dragged_card = null
func highlight_card(card):
# Highlights the card
card.move_rotate(card.position + transform.y * -HIGHLIGHT_HEIGHT, card.rotation, REORGANISE_TIME)
At the moment, the only issue is that dropping a sprite when there's another sprite under my cursor triggers the click event of the sprite isn't being dropped.
To be frank, the code is pretty much okay for what I'm doing. I'm asking here to see if anyone knows a better way to code those mechanics.
func _process(delta):
# Checks for clicks and hold, calls function based on events
if not owning_hand or not owning_hand.my_hand:
is_holding = false
return
if Input.is_action_just_released("click"):
if hold_time < HOLD_TARGET and input_in_sprite and is_holding:
owning_hand.clicked_cards.append(self)
if owning_hand.dragged_card:
owning_hand.drop_card()
is_holding = false
if Input.is_action_just_pressed("click") and input_in_sprite:
is_holding = true
if is_holding:
hold_time += delta
else:
hold_time = 0
if hold_time >= HOLD_TARGET:
owning_hand.held_cards.append(self)
Coding it like this seems to make it work correctly.
Basically, I used the is_holding bool to gather knowledge on if the sprite has been pressed earlier. If not, the is_action_just_released should completely ignore any action on the sprite.
Feel free to suggest better ways to implement this
I'm trying to temporarily disable touch on the entire screen, despite their being many sprites with touchesBegun onscreen.
I thought, obviously wrongly, turning off touch for the scene would do it:
scene?.isUserInteractionEnabled = false
But that didn't work, so I tried this, which also didn't work:
view?.scene?.isUserInteractionEnabled = false
That also didn't work, so I tried this, also from inside the scene:
self.isUserInteractionEnabled = false
There is no global method to turn off the touch, whatever is at the top of the drawing queue is the first responder.
You need to iterate through all of your nodes from your scene and turn them off:
enumerateChildNodesWithName("//*", usingBlock:
{ (node, stop) -> Void in
node.isUserInteractionEnabled = false
})
Now the problem is turning them back on, if you use this method, you will turn it on for everything, so you may want to adopt a naming convention for all your touchable sprites
enumerateChildNodesWithName("//touchable", usingBlock:
{ (node, stop) -> Void in
node.isUserInteractionEnabled = true
})
This will look for any node that has a name that begins with touchable.
This method involves recursion, so if you have a ton of nodes, it can be slow. Instead you should use an alternative method:
let disableTouchNode = SKSpriteNode(color:SKColor(red:0.0,green:0.0,blue:0.0,alpha:0.1),size:self.size)
disableTouchNode.isUserinteractionEnabled = true
disableTouchNode.zPosition = 99999
self.addChild(disableTouchNode)
What this does is slap on an almost transparent node on top of all elements the size of the scene. This way when a user touches the screen, this node will absorb it instead of anything else.
The following will disable all touches
self.view?.isUserInteractionEnabled = false
I have a for loop that will generate a trail of SKSpriteNodes with NO physics body
for _ in 1..<25 {
drawBlocks()
}
I have a playerObject (a ball) that HAS a physics body.
I'm trying to run a method that when the playerObject intersects one of the SKSPriteNodes generated from my for loop, the SKSpriteNode will disappear.
In my update method I have this
if block.frame.intersects(playeObject.frame) == true {
blocks.removeFromParent()
}
I tried to add a print statement if the method is actually being called when the two intersect but they don't. Any leads?
You could also make a physicsBody to your blocks without give to these block the dynamic property set to true (false = static object),so for each block:
block.physicsBody = SKPhysicsBody.init(edgeLoopFromRect: block.frame)
block.physicsBody?.mass = 1
block.physicsBody?.affectedByGravity = false
block.physicsBody?.restitution = 1
block.physicsBody?.dynamic = false
block.physicsBody?.usesPreciseCollisionDetection = true
block.physicsBody?.categoryBitMask = CollisionTypes.Block.rawValue
block.physicsBody?.contactTestBitMask = CollisionTypes. PlayerObject.rawValue
About your CollisionTypes could be:
enum CollisionTypes: UInt32 {
case Block = 1
case PlayerObject = 2
}
Add to your playerObject.physicsBody?.contactTestBitMask also the block , and check for the contacts between blocks and player directly to didBeginContact
Physics would make this easier, though would require more processing. Using physics you would have the option to use didBeginContact() notify you of a contact. Note that your player object would need to be dynamic, while the other objects could be static (dynamic = false). Static objects do not generate collisions with other static objects.
Without using physics what you have should work You will need to check for a collision in update().
I have a (moving) sprite node.
I'd like to detect other (moving) sprite nodes within a certain range of this node. Once one is detected, it should execute an action.
The playing an action part is no problem for me but I can't seem to figure out the within-range detection. Does have any ideas how to go about this?
A simple, but effective way to do this is comparing the position's in your scene's didEvaluateActions method. didEvaluateActions gets called once a frame (after actions have been evaluated but before physics simulation calculations are run). Any new actions you trigger will start evaluating on the next frame.
Since calculating the true distance requires a square root operation (this can be costly), we can write our own squaredDistance and skip that step. As long as our range/radius of detect is also squared, our comparisons will work out as expected. This example shows detect with a "true range" of 25.
// calculated the squared distance to avoid costly sqrt operation
func squaredDistance(p1: CGPoint, p2: CGPoint) -> CGFloat {
return pow(p2.x - p1.x, 2) + pow(p2.x - p1.x, 2)
}
// override the didEvaluateActions function of your scene
public override func didEvaluateActions() {
// assumes main node is called nodeToTest and
// all the nodes to check are in the array nodesToDetect
let squaredRadius: CGFloat = 25 * 25
for node in nodesToDetect {
if squareDistance(nodeToTest.position, p2: node.position) < squaredRadius {
// trigger action
}
}
}
If the action should only trigger once, you'll need to break out of the loop after the first detection and add some sort of check so it does not get triggered again on the next update without the proper cool down period. You may also need to convert the positions to the correct coordinate system.
Also, take a look at the documentation for SKScene. Depending on your setup, didEvaluateActions might not be the best choice for you. For example, if your game also relies on physics to move your nodes, it might be best to move this logic to didFinishUpdate (final callback before scene is rendered, called after all actions, physics simulations and constraints are applied for the frame).
Easiest way I can think of without killing performance is to add a child SKNode with an SKPhysicsBody for the range you want to hit, and use this new nodes contactBitMask to determine if they are in the range.
Something like this (pseudo code):
//Somewhere inside of setup of node
let node = SKNode()
node.physicsBody = SKPhysicsBody(circleOfRadius: 100)
node.categoryBitMask = DistanceCategory
node.contactBitMask = EnemyCategory
sprite.addNode(node)
//GameScene
func didBeginContact(...)
{
if body1 contactacted body2
{
do something with body1.node.parent
//we want parent because the contact is going to test the bigger node
}
}
I'm new to objective c and swift and I created a small app where small circles are rendered and once the player collides with a circle, the game ends. I managed to get everything to work, but how do I remove the nodes after they collide. I tried removeAllChildren(), but none of them disappear. When I use removeFromParent(), only 1 disappears. I want a way to remove all 3 nodes that will be rendered in the code below
//addEvilGuys() is called first
func addEvilGuys()
{
addEvilGuy(named: "paul", speed: 1.3, xPos: CGFloat(self.size.height/3))
addEvilGuy(named: "boris", speed: 1.7, xPos: frame.size.width/4 + 50)
addEvilGuy(named: "natasha", speed: 1.5, xPos: frame.size.width/4 + 150)
}
func addEvilGuy(#named:String, speed:Float, xPos: CGFloat)
{
evilGuyNode = SKSpriteNode(imageNamed: named)
evilGuyNode.zPosition = 10
evilGuyNode.physicsBody = SKPhysicsBody(circleOfRadius: 16)
evilGuyNode.physicsBody!.affectedByGravity = false
evilGuyNode.physicsBody!.categoryBitMask = ColliderType.BadGuy.rawValue
evilGuyNode.physicsBody!.contactTestBitMask = ColliderType.Hero.rawValue
evilGuyNode.physicsBody!.collisionBitMask = ColliderType.Hero.rawValue
evilGuyNodeCount++
var evilGuy = EvilGuy(speed: speed, eGuy: evilGuyNode)
evilGuys.append(evilGuy)
resetEvilGuy(evilGuyNode, xPos: xPos)
evilGuy.xPos = evilGuyNode.position.x
addChild(evilGuyNode)
}
func resetEvilGuy(evilGuyNode:SKSpriteNode, xPos:CGFloat)
{
evilGuyNode.position.y = endOfScreenBottom
evilGuyNode.position.x = xPos
}
It looks like in addEvilGuy you are recreating a stored property (i.e. that is visible for the entire class + whatever the access level allows) to create the SKSpriteNode that you're adding. This means that you are orphaning the previously created EvilGuy.
In addEvilGuy, replace
evilGuyNode = SKSpriteNode(imageNamed: named)
with
let evilGuyNode = SKSpriteNode(imageNamed: named)
and remove the property from your class (it doesn't seem like you have a need for in in a larger scope).
It also looks like you're creating EvilGuys and storing them in an array, which is good. So when you can remove all of them from the screen with a function like:
func removeAllEvilGuys(evilGuys: [EvilGuy]) {
for evilGuy in evilGuys {
evilGuy.eGuy.removeFromParent()
}
}
As a best practice advice, since you mentioned you're a beginner:
I'd recommend defining the characteristics of the evil guys in a .plist and then use the file to create an array of evil guys. This way you can easily make changes to the evil guys in that file without having to change anything in your code.
The code that creates an EvilGuy object should be separated from the one that adds the evil guy to the screen. As long as you are storing the SKNode of each one, you'll be able to add/remove without unnecessarily recreating the entire object.