I am building my first game with SpriteKit, written in Swift 3.
I have been trying for hours to accomplish this "winning stars effect", but without any acceptable result yet :/
The effect i am looking for is demonstrated in this video: https://youtu.be/9CeK5_G8T9E
It's not a problem adding one or multiple stars; the problem is to make them move in different directions by different paths to the same location, just like it's done in this video example.
Hope for your help to lead me in the right direction.
Any solution, tutorials, examples etc. is more than welcome.
Thanks for your time.
Took a while but here's something:
import SpriteKit
class GameScene: SKScene {
var sprite = SKSpriteNode()
override func didMove(to view: SKView) {
for _ in 1...15 {
spawnsprite()
}
}
func spawnsprite() {
sprite = SKSpriteNode(color: SKColor.yellow, size: CGSize(width: 50, height: 50))
sprite.position = CGPoint(x: (-size.width/2)+50, y: (-size.height/2)+50)
addChild(sprite)
let destination = CGPoint(x: (size.width/2)-50, y: (size.height/2)-50)
let path = createCurvedPath(from: sprite.position, to: destination, varyingBy: 500)
let squareSpeed = CGFloat(arc4random_uniform(600)) + 200
let moveAction = SKAction.follow(path, asOffset: false, orientToPath: false, speed: squareSpeed)
let rotateAction = SKAction.repeatForever(SKAction.rotate(byAngle: 2 * CGFloat.pi, duration: 2))
sprite.run(SKAction.group([moveAction, rotateAction]))
}
func createCurvedPath(from start: CGPoint, to destination: CGPoint, varyingBy offset: UInt32) -> CGMutablePath {
let pathToMove = CGMutablePath()
pathToMove.move(to: start)
let controlPoint = CGPoint(x: CGFloat(arc4random_uniform(offset)) - CGFloat(offset/2),
y: CGFloat(arc4random_uniform(offset)) - CGFloat(offset/2))
pathToMove.addQuadCurve(to: destination, control: controlPoint)
return pathToMove
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
There's a lot that can be done to make this nicer (e.g. subclass SKSpriteNode and make the path that the ode follows a property of the sprite itself), but it illustrates the principle.
It could probably also be done with an SKEmitter.
Related
I'd like to create animation of the line chart like drawing from start point to end point.
I've created InsettableShape (code below) in order to use with strokeBorder(), but can't figure out how to define animatableData.
Thank you!
(I know I can do it with Shape and trim(), but I need inset.)
import SwiftUI
struct LineChartInsettableShape: InsettableShape {
let series: [CGFloat]
var insetAmount: CGFloat = 0
func inset(by amount: CGFloat) -> some InsettableShape {
var chart = self
chart.insetAmount += amount
return chart
}
func path(in rect: CGRect) -> Path {
guard !series.isEmpty else { return Path() }
let minY = series.min()!
let maxY = series.max()!
let xStep = (rect.width - insetAmount * 2) / CGFloat(series.count - 1)
func point(index: Int) -> CGPoint {
let yValue: CGFloat = CGFloat(series[index])
return CGPoint(
x: xStep * CGFloat(index) + insetAmount,
y: (rect.height - insetAmount * 2) * (1 - (yValue - minY) / (maxY - minY)) + insetAmount
)
}
return Path { path in
path.move(to: point(index: 0))
for index in 1..<series.count {
path.addLine(to: point(index: index))
}
}
}
}
It's not hard once you figure it out, but initially how this works can be a bit confusing. You need to have a property named animatableData which tells SwiftUI the name of the variable you'll be using for animation, and how to get it and set its value. For example, I have a small piece of animation code that uses a variable named inset, which is very similar to your insetAmount. Here's all the code my app needs to use this variable for animation:
var inset: CGFloat
var animatableData: CGFloat {
get { inset }
set { self.inset = newValue}
}
Not all data types can be used for animation. CGFloat, used here, certainly can be. Paul Hudson has an excellent short video on this topic: https://www.hackingwithswift.com/books/ios-swiftui/animating-simple-shapes-with-animatabledata.
There is a bar chart library, you can check it out as well https://github.com/dawigr/BarChart
I'm new to programming with SpriteKit and Swift. I have followed a few tutorials, but I'm now stucking for a couple of days.
i have multiple animations in an separate Actions file, "idle", "run" and jump. When my games start you see the idle figure and when you tap it will move from idle animation to the running animation. So far so good. But when my figure start jumping with next tap, the running animation is also still on my screen. When the jumps finish the jump animation is gone. How can I remove the running animation for a second?
So far the only other option I've got is to remove also the running animation as the jump animation. They won't comeback.
This is my hero file where if have the actions for running and jumping:
func runPlayer(){
let playerRun:SKAction = SKAction(named: "run", duration: moveSpeed)!
let runAction:SKAction = SKAction.moveBy(x: 0, y: 0, duration: moveSpeed)
let runGroup:SKAction = SKAction.group([playerRun, runAction])
thePlayerRuns.run(runGroup, withKey: "run")
thePlayerRuns.physicsBody?.isDynamic = false
thePlayerRuns.position = CGPoint(x: 0 , y: 0)
thePlayerRuns.size = CGSize(width: 160.25, height: 135.55)
thePlayerRuns.zPosition = 99
addChild(thePlayerRuns)
}
func jumpPlayer(){
let playerJump: SKAction = SKAction(named: "jump", duration: moveSpeed)!
let jumpUp: SKAction = SKAction.moveBy(x: 0, y: 50, duration: 0.5)
let jumpDown: SKAction = SKAction.moveBy(x: 0, y: -80, duration: 0.5)
let jumpSeq: SKAction = SKAction.sequence([jumpUp, jumpDown])
let jumpGroup: SKAction = SKAction.group([playerJump, jumpSeq])
thePlayerJumps.run(jumpGroup, withKey: "jump")
thePlayerJumps.physicsBody?.affectedByGravity = true
thePlayerJumps.physicsBody?.allowsRotation = false
thePlayerJumps.physicsBody?.isDynamic = true
thePlayerJumps.position = CGPoint(x: 0 , y: 30)
thePlayerJumps.size = CGSize(width: 160.25, height: 135.55)
thePlayerJumps.zPosition = 99
addChild(thePlayerJumps)
thePlayerRuns.removeFromParent()
thePlayerJumps.run(
SKAction.sequence([SKAction.wait(forDuration: 1.0),SKAction.removeFromParent()])
)
// thePlayerRuns.run(
// SKAction.sequence([SKAction.removeFromParent()])
//
// )
let otherWait = SKAction.wait(forDuration: 1)
let otherSequence = SKAction.sequence([otherWait, SKAction(runPlayer())])
run(otherSequence)
/*
let otherWait = SKAction.waitForDuration(1)
let otherSequence = SKAction.sequence([otherWait, SKAction.repeatActionForever(sequence)])
runAction(otherSequence)
*/
}
And this is my Gamescene:
hero = MTHero()
hero.position = CGPoint(x: 70, y: 278)
addChild(hero)
hero.idlePlayer()
}
func start(){
isStarted = true
hero.stop()
movingGround.start()
hero.runPlayer()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if !isStarted {
start()
}else{
hero.jumpPlayer()
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
I thing I have to do something in thouchesBagan with another else statement.
Thanks in advance! Moi
I've managed to solve it. I'm not sure that it is the best option. But in my jumpPlayer function I have added the following code:
self.run(SKAction.wait(forDuration: 1)) {
self.addChild(self.thePlayerRuns)
just under
thePlayerJumps.run(
SKAction.sequence([SKAction.wait(forDuration: 1.0),SKAction.removeFromParent()])
)
If you have suggestion for a better solution, please let me know.
Modus Operandi:
1) Use an UIImageView of a base Clock Image.
2) Add MinuteHand & HourHand sublayers (containing their respective images) to the UIImageView layer.
Problem: both sublayers disappear when attempting to perform a rotation transformation.
Note: 1) I've removed the 'hour' code & ancillary radian calculations to simplify code.
2) The 'center' is the center of the clock. I had adjusted the coordinates to actually pin the hands to the clock's center.
3) The ViewDidLayoutSubviews() appear to be okay. I got the clock + hands.
class ClockViewController:UIViewController {
private let minuteLayer = CALayer()
#IBOutlet weak var clockBaseImageView: UIImageView!
#IBOutlet weak var datePicker: UIDatePicker!
override func viewDidLayoutSubviews() {
guard var minuteSize = UIImage(named: "MinuteHand")?.size,
var hourSize = UIImage(named: "HourHand")?.size
else {
return
}
var contentLayer:CALayer {
return self.view.layer
}
var center = clockBaseImageView.center
// Minute Hand:
minuteLayer.setValue("*** Minute Hand ***", forKey: "id")
minuteSize = CGSize(width: minuteSize.width/3, height: minuteSize.height/3)
minuteLayer.contents = UIImage(named: "MinuteHand")?.cgImage
center = CGPoint(x: 107.0, y: 40.0)
var handFrame = CGRect(origin: center, size: minuteSize)
minuteLayer.frame = handFrame
minuteLayer.contentsScale = clockBaseImageView.layer.contentsScale
minuteLayer.anchorPoint = center
clockBaseImageView.layer.addSublayer(minuteLayer)
}
Here's my problem: Attempting to rotate the minute hand via 0.01 radians:
func set(_ time:Date) {
minuteLayer.setAffineTransform(CGAffineTransform(rotationAngle: .01)) // random value for test.
}
Before rotation attempt:
After attempting to rotate minute hand:
The hand shifted laterally to the right vs rotate.
Why? Perhaps due to the pivot point?
I think this will solve your problem, Take a look and let me know.
import GLKit // Importing GLKit Framework
func set(_ time:Date) {
minuteLayer.transform = CGAffineTransformMakeRotation(CGFloat(GLKMathDegreesToRadians(0.01)))
}
Note: this solution doesn't solve the issue about rotating a CALayer. Instead, it bypasses the issue by replacing the layer with a subview and rotating the subview via:
func set(_ time:Date) {
minuteView.transform = CGAffineTransform(rotationAngle: 45 * CGFloat(M_PI)/180.0)
}
Here's the result:
Still, it would be nice to know how to rotate a CALayer.
In my game, the position of my SKNodes slightly change when I run the App on a virtual simulator vs on a real device(my iPad).
Here are pictures of what I am talking about.
This is the virtual simulator
This is my Ipad
It is hard to see, but the two red boxes are slightly higher on my iPad than in the simulator
Here is how i declare the size and position of the red boxes and green net:
The following code is located in my GameScene.swift file
func loadAppearance_Rim1() {
Rim1 = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake((frame.size.width) / 40, (frame.size.width) / 40))
Rim1.position = CGPointMake(((frame.size.width) / 2.23), ((frame.size.height) / 1.33))
Rim1.zPosition = 1
addChild(Rim1)
}
func loadAppearance_Rim2(){
Rim2 = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake((frame.size.width) / 40, (frame.size.width) / 40))
Rim2.position = CGPoint(x: ((frame.size.width) / 1.8), y: ((frame.size.height) / 1.33))
Rim2.zPosition = 1
addChild(Rim2)
}
func loadAppearance_RimNet(){
RimNet = SKSpriteNode(color: UIColor.greenColor(), size: CGSizeMake((frame.size.width) / 7.5, (frame.size.width) / 150))
RimNet.position = CGPointMake(frame.size.width / 1.99, frame.size.height / 1.33)
RimNet.zPosition = 1
addChild(RimNet)
}
func addBackground(){
//background
background = SKSpriteNode(imageNamed: "Background")
background.zPosition = 0
background.size = self.frame.size
background.position = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
self.addChild(background)
}
Additionally my GameViewController.swift looks like this
import UIKit
import SpriteKit
class GameViewController: UIViewController {
var scene: GameScene!
override func viewDidLoad() {
super.viewDidLoad()
//Configure the view
let skView = view as! SKView
//If finger is on iphone, you cant tap again
skView.multipleTouchEnabled = false
//Create and configure the scene
//create scene within size of skview
scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
scene.size = skView.bounds.size
//scene.anchorPoint = CGPointZero
//present the scene
skView.presentScene(scene)
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .Landscape
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
How can I make the positions of my nodes be the same for each simulator/physical device?
You should round those floating point values to integers via a call to (int)round(float) so that the values snap to whole pixels. Any place where you use CGPoint or CGSize should use whole pixels as opposed to floating point values.
If you are making a Universal application you need to declare the size of the scene using integer values. Here is an example:
scene = GameScene(size:CGSize(width: 2048, height: 1536))
Then when you initialize the positions and sizes of your nodes using CGPoint and CGSize, make them dependant on SKScene size. Here is an example:
node.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
If you declare the size of the scene for a Universal App like this:
scene.size = skView.bounds.size
then your SKSpriteNode positions will be all messed up. You may also need to change the scaleMode to .ResizeFill. This worked for me.
I am trying to get my nodes to move across the screen and spawn infinitely on a timer. I would aslo like to have multiple nodes on screen at once. This is my stable code so far. I have tried multiple different ways with either freezes or crashes.
let bunnyTexture = SKTexture(imageNamed: "oval 1.png")
bunny = SKSpriteNode(texture: bunnyTexture)
bunny.position = CGPoint(x: (size.width / 3), y: 750 + bunny.frame.height)
self.addChild(bunny)
bunny.physicsBody = SKPhysicsBody(circleOfRadius: bunnyTexture.size().height/2)
bunny.physicsBody!.dynamic = true
let spawnBunny = SKAction.moveByX(0, y: -self.frame.size.height, duration: 4)
let despawnBunny = SKAction.removeFromParent()
let spawnNdespawn2 = SKAction.sequence([spawnBunny, despawnBunny])
bunny.runAction(spawnNdespawn2)
let gobblinTexture = SKTexture(imageNamed: "oval 2.png")
gobblin = SKSpriteNode(texture: gobblinTexture)
gobblin.position = CGPoint(x: 150 + gobblin.frame.width, y: (size.height / 3))
self.addChild(gobblin)
let randomGob = arc4random() % UInt32(self.frame.size.height / 2)
let spawnGob = SKAction.moveByX(+self.frame.size.width, y: 0, duration: 4)
let despawnGob = SKAction.removeFromParent()
let spawnNdespawn = SKAction.sequence([spawnGob, despawnGob])
gobblin.runAction(spawnNdespawn)
let ground = SKNode()
ground.position = CGPointMake(0, 0)
ground.physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(self.frame.size.width, -50))
ground.physicsBody!.dynamic = false
self.addChild(ground)
You can use SKAction.waitForDuration(interval) to create a looping timer.
Extend SKNode with a new function loopAction. This takes a SKAction an NSTimeInterval and a () -> Bool function.
The SKAction is the function that will be executed at each interval.
The () -> Bool function will be used to stop the otherwise infinite loop.
Just remember that this captures both the SKNode and the SKAction. Neither will deallocate until the loop is stopped.
Its possible to create an object with a flag (Bool) to hold all this information and set the flag to false when it needs to stop. I just prefer this way.
LoopActionManager is an example of how the loopAction is implemented.
You need an SKNode and an SKAction. Instead of calling runAction on the SKNode you call loopAction and pass the interval and the function that controls when to stop. You would place this code somewhere you have access to the nodes in question.
The function to stop can also be implemented as a trailing closure
Paste the code into a playground to see how it works.
import UIKit
import SpriteKit
import XCPlayground
extension SKNode {
func loopAction(action:SKAction,interval:NSTimeInterval,continueLoop:() -> Bool) {
guard continueLoop() else { return }
runAction(SKAction.waitForDuration(interval)) {
if continueLoop() {
self.runAction(action)
self.loopAction(action, interval: interval, continueLoop: continueLoop)
}
}
}
}
// example
class LoopActionManager {
let node = SKSpriteNode(color: UIColor.whiteColor(), size: CGSize(width: 20, height: 20))
let action = SKAction.moveBy(CGVector(dx: 10,dy: 0), duration: 0.5)
func continueMoveLoop() -> Bool {
return (node.position.x + node.size.width) < node.scene?.size.width
}
func start() {
node.loopAction(action, interval: 1, continueLoop: continueMoveLoop)
}
}
let view = SKView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let example = LoopActionManager()
scene.addChild(example.node)
example.start()
XCPlaygroundPage.currentPage.liveView = view
As stated in the comments below it is also possible to use repeatAction.
You can extend SKAction with a static func to generate the repeatAction based on an action to repeat and an interval.
In comparison to the first solution this is simpler and more in line with the SDK. You do lose the completion handler for each interval.
import UIKit
import SpriteKit
import XCPlayground
extension SKAction {
static func repeatAction(action:SKAction,interval:NSTimeInterval) -> SKAction {
// diff between interval and action.duration will be the wait time. This makes interval the interval between action starts. Max of 0 and diff to make sure it isn't smaller than 0
let waitAction = SKAction.waitForDuration(max(0,interval - action.duration))
let sequenceAction = SKAction.sequence([waitAction,action])
let repeatAction = SKAction.repeatActionForever(sequenceAction)
return repeatAction
}
}
let view = SKView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let node = SKSpriteNode(color: UIColor.whiteColor(), size: CGSize(width: 20, height: 20))
let action = SKAction.moveBy(CGVector(dx: 10,dy: 0), duration: 0.5)
scene.addChild(node)
node.runAction(SKAction.repeatAction(action, interval: 1.0))
XCPlaygroundPage.currentPage.liveView = view