I am trying to work out how to spawn a SKSpriteNode between two positions (located on the screen) randomly in Swift.
This is what I came up with, but it does spawn Nodes outside the screen dimensions altough I defined two points inside the screen bounds!
/* Spawning Values */
var MinSpawnValue = self.size.width / 8
var MaxSpawnValue = self.size.width - self.size.width / 8
var SpawnPosition = UInt32(MaxSpawnValue - MinSpawnValue)
/* Node Positioning */
Node.position = CGPointMake(CGFloat(arc4random_uniform(SpawnPosition)),CGRectGetMidY(self.frame))
You just need to add your random value to MinSpawnValue. This may need some adjusting for types:
Node.position = CGPointMake(CGFloat(arc4random_uniform(SpawnPosition)) + MinSpawnValue,CGRectGetMidY(self.frame))
I find it easier to think about things like this by using actual numbers. For instance, if MinSpawnValue were 1000 and MaxSpawnValue were 1004, then SpawnPosition would be 4 and you would generate a number in the range 0...3. By adding that number to MinSpawnValue, you'd get a number in the range 1000...1003.
Related
I have a function that spawns little balls, randomly positioned, on the screen. The problem I face is that I want to distribute the balls randomly, but when I do so, some balls spawn on top of each other. I want to exclude all the positions that are already taken (and maybe a buffer of a few pixels around the balls), but I don't know how to do so. I worked around this by giving the balls a Physicsbody, so they move off from one another if they happen to spawn on top of each other. But I want them to not spawn on top of each other in the first place. My code for now is the following:
spawnedBalls = [Ball]()
level = Int()
func setupLevel() {
let numberOfBallsToGenerate = level * 2
let boundary: CGFloat = 26
let rightBoundary = scene!.size.width - boundary
let topBoundary = scene!.size.height - boundary
while spawnedBalls.count < numberOfBallsToGenerate {
let randomPosition = CGPoint(x: CGFloat.random(in: boundary...rightBoundary), y: CGFloat.random(in: boundary...topBoundary))
let ball = Ball()
ball.position = randomPosition
ball.size = CGSize(width: 32, height: 32)
ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.width)
ball.physicsBody?.affectedByGravity = false
ball.physicsBody?.allowsRotation = false
ball.physicsBody?.categoryBitMask = 1
ball.physicsBody?.collisionBitMask = 1
spawnedBalls.append(ball)
self.addChild(ball)
}
}
I don't know if this problem should be solved by having an array that stores all taken positions, or if I should use some kind of FiledNode, where occupied space can be sort of subtracted, but sadly I am unfamiliar with FieldNodes, so I don't know if that's the right way to face the problem.
Step 1) Replace
let randomPosition = ....
with
let randomPosition = randomPositionInOpenSpace()
Step 2) Write the randomPositionInOpenSpace function:
Idea is:
a) generate a random position
b) is it in open space? if so return that
c) repeat until OK
Then Step 3) write the 'is it in open space' function
For that you need to know if the proposed coordinate is near any of the other balls. For circles, you can test the distance between their centers is greater than (radiuses + margins). Distance between centers is pythagoras: sqrt of the x delta squared plus the y delta squared.
I'm trying to create a game using Apple's SpriteKit game engine.
While implementing some physics-based calculations in the game, I noticed that the calculated results differ from what effectively then happens to objects.
Example: calculating a body's trajectory through projectile motion's equations causes the body to actually fall down much sooner/quicker than what calculated.
How can I make the physics engine match the real-world physics laws when calculating something gravity-related?
I think I know what's going on with the sample code you have supplied on GitHub, which I'll reproduce here as questions on SO should contain the code:
//
// GameScene.swift
// SpriteKitGravitySample
//
// Created by Emilio Schepis on 17/01/2020.
// Copyright © 2020 Emilio Schepis. All rights reserved.
//
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var subject: SKNode!
override func didMove(to view: SKView) {
super.didMove(to: view)
// World setup (no friction, default gravity)
// Note that this would work with any gravity set to the scene.
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody?.friction = 0
subject = SKShapeNode(circleOfRadius: 10)
subject.position = CGPoint(x: frame.midX, y: 30)
subject.physicsBody = SKPhysicsBody(circleOfRadius: 10)
subject.physicsBody?.allowsRotation = false
// Free falling body (no damping)
subject.physicsBody?.linearDamping = 0
subject.physicsBody?.angularDamping = 0
addChild(subject)
// Set an arbitrary velocity to the body
subject.physicsBody?.velocity = CGVector(dx: 30, dy: 700)
// Inaccurate prediction of position over time
for time in stride(from: CGFloat(0), to: 1, by: 0.01) {
let inaccuratePosition = SKShapeNode(circleOfRadius: 2)
inaccuratePosition.strokeColor = .red
// These lines use the projectile motion equations as-is.
// https://en.wikipedia.org/wiki/Projectile_motion#Displacement
let v = subject.physicsBody?.velocity ?? .zero
let x = v.dx * time
let y = v.dy * time + 0.5 * physicsWorld.gravity.dy * pow(time, 2)
inaccuratePosition.position = CGPoint(x: x + subject.position.x,
y: y + subject.position.y)
addChild(inaccuratePosition)
}
// Actual prediction of position over time
for time in stride(from: CGFloat(0), to: 1, by: 0.01) {
let accuratePosition = SKShapeNode(circleOfRadius: 2)
accuratePosition.strokeColor = .green
// These lines use the projectile motion equations
// as if the gravity was 150 times stronger.
// The subject follows this curve perfectly.
let v = subject.physicsBody?.velocity ?? .zero
let x = v.dx * time
let y = v.dy * time + 0.5 * physicsWorld.gravity.dy * 150 * pow(time, 2)
accuratePosition.position = CGPoint(x: x + subject.position.x,
y: y + subject.position.y)
addChild(accuratePosition)
}
}
}
What you've done is to:
Created an object called subject with a physicsBody and placed it
on screen with a initial velocity.
Plotted predicted positions for an object with that velocity under
gravity via the inaccuratePosition node, using Newton's laws of
motion (v = ut + 1/2at²)
Plotted predicted positions for an object with that velocity under
gravity * 150 via the accuratePosition node, using Newton's laws of
motion
All this is is didMoveTo. When the simulation runs, the path of the node subject follows the accuratePosition path accurately.
I think what's happening is that you are calculating the predicted position using subject's physicsBody's velocity, which is in m/s, but the position is in points, so what you should do is convert m/s into point/s first.
So what's the scale factor? Well from Apple's documentation here; it's.... 150 which is too much of a coincidence 😀, so I think that's the problem.
Bear in mind that you set the vertical velocity of your object to 700m/s - that's 1500mph or 105000 SK point/s. You'd expect it to simply disappear out through the top of the screen at high speed, as predicted by your red path. The screen is somewhere between 1,000 and 2,000 points.
Edit - I created a sample project to demonstrate the calculated paths with and without the multiplier.
https://github.com/emilioschepis/spritekit-gravity-sample
TL;DR - When calculating something gravity-related in SpriteKit multiply the gravity of the scene by 151 to obtain an accurate result.
When trying to solve this issue I first started reading the SpriteKit documentation related to gravity:
https://developer.apple.com/documentation/spritekit/skphysicsworld/1449623-gravity
The documentation says:
The components of this property are measured in meters per second. The default value is (0.0,-9.8), which represent’s Earth’s gravity.
Gravity, however is calculated in m/s^2 and not in m/s.
Thinking that was an error in the implementation of gravity in SpriteKit I began thinking that maybe real-world-based physics laws could not be applied in the scene.
I did, however, come across another documentation page about the linear gravity field that correctly reported that gravity is measured in m/s^2 in SpriteKit.
https://developer.apple.com/documentation/spritekit/skfieldnode/1520145-lineargravityfield
I then setup a simple free falling scene where I applied an initial velocity to a physics body and then calculated the expected trajectory, while comparing it to the actual trajectory.
The x-axis calculations were accurate from the start, suggesting that the only problem was with the gravity's value.
I then tried manually modified the gravity in the scene until the actual trajectory matched the predicted one.
What I found is that there is a "magic" value of ~151 that has to be factored in when using the physics world's gravity property in the game.
Modifying, for example, the y-axis calculations for the trajectory from
let dy = velocity.dy * time + 0.5 * gravity * pow(time, 2)
to
let dy = velocity.dy * time + 0.5 * 151 * gravity * pow(time, 2)
resulted in accurate calculations.
I hope this is useful to anyone who might encounter the same problem in the future.
Apologies in advance as I'm not sure exactly what the right question is. The problems that I'm ultimately trying to address are:
1) Game gets laggy at times
2) CPU % can get high, as much as 50-60% at times, but is also sometimes relatively low (<20%)
3) Device (iPhone 6s) can get slightly warm
I believe what's driving the lagginess is that I'm constantly creating and removing circles in the SKScene. It's pretty much unavoidable because the circles are a critical element to the game and I have to constantly change their size and physicsBody properties so there's not much I can do in terms of reusing nodes. Additionally, I'm moving another node almost constantly.
func addCircle() {
let attributes = getTargetAttributes() //sets size, position, and color of the circle
let target = /*SKShapeNode()*/SKShapeNode(circleOfRadius: attributes.size.width)
let outerPathRect = CGRect(x: 0, y: 0, width: attributes.size.width * 2, height: attributes.size.width * 2)
target.position = attributes.position
target.fillColor = attributes.color
target.strokeColor = attributes.stroke
target.lineWidth = 8 * attributes.size.width / 35
target.physicsBody = SKPhysicsBody(circleOfRadius: attributes.size.width)
addStandardProperties(node: target, name: "circle", z: 5, contactTest: ContactCategory, category: CircleCategory) //Sets physicsBody properties
addChild(target)
}
The getAttributes() function is not too costly. It does have a while loop to set the circle position, but it doesn't usually get used when the function is called. Otherwise, it's simple math.
Some other details:
1) The app runs at a constant 120 fps. I've tried setting the scene/view lower by adding view.preferredFramesPerSecond = 60 in GameScene.swift and gameScene.preferredFramesPerSecond = 60 in GameViewController. Neither one of these does anything to change the fps. Normally when I've had performance issues in other apps, the fps dipped, however, that isn't happening here.
2) I’ve tried switching the SKShapeNode initializer to use a path versus circleOfRadius and then resetting the path. I’ve also tried images, however, because I have to reset the physicsBody, there doesn’t appear to be a performance gain.
3) I tried changing the physicsWorld speed, but this also had little effect.
4) I've also used Instruments to try to identify the issue. There are big chunks of resources being used by SKRenderer, however, I can't find much information on this.
Creating SKShapeNodes are inefficient, try to use it as few times as you can. instead, create a template shape, and convert it to an SKSpriteNode.
If you need to change the size, then use xScale and yScale, if you need to change the color, then use color with colorBlendFactor of 1
If you need to have a varying color stroke, then change the below code to have 2 SKSpriteNodes, 1 SKSpriteNode that handles only the fill, and 1 SKSpriteNode that handles only the stroke. Have the stroke sprite be a child of the fill sprite with a zPosition of 0 and set the stroke color to white. You can then apply the color and colorBlendFactor to the child node of the circle to change the color.
lazy var circle =
{
let target = SKShapeNode(circleOfRadius: 1000)
target.fillColor = .white
//target.strokeColor = .black //if stroke is anything other than black, you may need to do 2 SKSpriteNodes that layer each other
target.lineWidth = 8 * 1000 / 35
let texture = SKView().texture(from:target)
let spr = SKSpriteNode(texture:texture)
spr.physicsBody = SKPhysicsBody(circleOfRadius: 1000)
addStandardProperties(node: spr, name: "circle", z: 5, contactTest:ContactCategory, category: CircleCategory) //Sets physicsBody properties
return spr
}()
func createCircle(of radius:CGFloat,color:UIColor) -> SKSpriteNode
{
let spr = circle.copy()
let scale = radius/1000.0
spr.xScale = scale
spr.yScale = scale
spr.color = color
spr.colorBlendFactor = 1.0
return spr
}
func addCircle() {
let attributes = getTargetAttributes() //sets size, position, and color of the circle
let spr = createCircle(of:attribute.width,color:attributes.color)
spr.position = attributes.position
addChild(str)
}
I'm creating a game using Swift 2.0 and Sprite-Kit with Xcode7. I want to implement 4 purple balls that are suppose to resemble lives. So every time the player gets hit he loses one purple ball. They are supposed to appear side by side. I was wondering if instead of hardcoding 4 balls on to the scene I could instead use a for loop to display 4 balls.
let purpleBall = SKSpriteNode(texture: purpleTexture)
purpleBall.position = CGPointMake(self.frame.size.width * 0.65, self.frame.size.height * 0.92)
self.addChild(purpleBall)
I haven't been successful on getting 4 balls to appear on the scene. This was one of my attempts.
for(var i = 0.50; i <= 0.90; i = i + 0.10) {
let purpleBall = SKSpriteNode(texture: purpleTexture)
purpleBall.position = CGPointMake(self.frame.size.width * i, self.frame.size.height * 0.92)
self.addChild(purpleBall)
}
Here I get an error: Binary operator cannot be applied to operands of type 'CGFloat' and 'Double'
Do I have to convert i to CGFloat? and will this code actually place 4 different balls side by side or only move the single one to each new position.
Your code has an off by one error in the for loop. It should be a less than sign rather than less than or equal too. Your current code is going to create five purple balls.
You shouldn't really let i equal anything but an integer it's confusing. You could rewrite the code like this and it would do the same thing, without the worry of the float/double errors you're currently getting. Note that I will also use a constant called maxBalls, and distanceBetweenBalls so that you can easily change the number of balls and the distance without any complicated rewriting:
let maxBalls = 4
for(var i = 0; i < maxBalls; i++) {
let purpleBall = SKSpriteNode(texture: purpleTexture)
let ballXPosition = .50 + (i * .1)
purpleBall.position = CGPointMake(self.frame.size.width * ballXPosition, self.frame.size.height * 0.92)
self.addChild(purpleBall)
This should avoid the issues you were facing before, hope that helps.
If I'm trying to spawn in enemies in 2 defined areas of the screen (top and bottom with a middle section where they can't spawn in), how do I prevent them from spawning on top of or too near each other .
My sprites are relatively quite small to the screen, and the only suggestion I've found on here is to create an array of possible positions and every time you use one of those positions take it off the list, but first of all I don't know how that would even look, second of all I've got SO many possible positions because I'm working with 5px high sprites, and I want for them to be able to respawn once that area is clear.
My method of choosing top or bottom is just picking a random number 1 or 2, and depending I've got 2 functions that make them either on top or on bottom.
I just need for no 2 objects to spawn in a ball's diameter from each other. Any ideas how to incorporate that into my spawning?
edit:
//Create your array and populate it with potential starting points
var posArray = Array<CGPoint>()
posArray.append((CGPoint(x: 1.0, y: 1.0))
posArray.append((CGPoint(x: 1.0, y: 2.0))
posArray.append((CGPoint(x: 1.0, y: 3.0))
//Generate an enemy by rolling the dice and
//remove its start position from our queue
let randPos = Int(arc4random()) % posArray.count
posArray[randPos]
posArray.removeAtIndex(randPos)
...
//Play game and wait for enemy to die
//Then repopulate the array with that enemy's start position
posArray.append(enemyDude.startPosition)
This is the recommendation I found, but this gives errors "expected separator" that I don't really know how to fix.
And so really, I'd have to make a HUGE array of possible positions going across the X and Y covering all areas, or is there some better way to do this?
Not spawning a node on top of another is simply enough by using intersectsNode(_ node: SKNode) -> Bool.
As for not spawning too close, that's another story. The only way you can do that is too have all your current nodes in an array, enumerate the array and check each node's position to that of the spawning node. Dependent on your parameters, you either spawn or not spawn.
I am not versed in Swift so you will have to translate the code yourself.
-(void)testMethod {
// an array with all your current nodes
NSMutableArray *myArray = [NSMutableArray new];
// the potential new spawn node with the proposed spawn position
SKSpriteNode *mySpawnNode = [SKSpriteNode new];
BOOL tooClose = NO;
// enumerate your node array
for(SKSpriteNode *object in myArray) {
// get the absoulte x and y position distance differences of the spawn node
// and the current node in the array
// using absolute so you can check both positive and negative values later
// in the IF statement
float xPos = fabs(object.position.x - mySpawnNode.position.x);
float yPos = fabs(object.position.y - mySpawnNode.position.y);
// check if the spawn position is less than 10 for the x or y in relation
// to the current node in the array
if((xPos < 10) || (yPos < 10))
tooClose = YES;
}
if(tooClose == NO) {
// spawn node
}
}
Note that the array should be a property and not declared in the scope I have it in the example.