Swift SpriteKit: Creating a realistic driving experience 2D - ios

How would I go by creating a realistic driving experience?
I'm using iOS Swift 3 with SpriteKit using the functions applyForce to accelerate when I click the gas button and when braking I add friction to the physicsBody, also I can't seem to turn right, no idea on how to do it.
Right now for turning I'm splitting the screen using the left and right to turn but I'm using applyForce but its very bad because it turns when the car is stopped and in a very unrealistic way.
When I apply force its only to go up so if I do create a turning mechanism and I do a uturn the car will still go up.
Also side note: Multitouch doesn't work?
Any help? Thanks
override func didMove(to view: SKView) {
// Multi Touch
self.view?.isMultipleTouchEnabled = true
car.carNode.position = CGPoint(x: 0, y: 0)
car.carNode.physicsBody = SKPhysicsBody(rectangleOf: car.carNode.size)
car.carNode.physicsBody?.affectedByGravity = false
car.carNode.physicsBody?.angularDamping = 0.1
car.carNode.physicsBody?.linearDamping = 0.1
car.carNode.physicsBody?.friction = 0.1
car.carNode.physicsBody?.mass = 1
self.addChild(car.carNode)
// HUD Display
gas.gasButton.position = CGPoint(x: 300, y: -500)
self.addChild(gas.gasButton)
brake.brakeButton.position = CGPoint(x: 150, y: -500)
self.addChild(brake.brakeButton)
}
override func update(_ currentTime: TimeInterval) {
if touching {
car.carNode.physicsBody?.applyForce(CGVector(dx: 0, dy: 180))
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if location.x < self.frame.size.width / 2 {
// Left side of the screen
car.carNode.physicsBody?.applyForce(CGVector(dx: -100, dy: 0))
} else {
// Right side of the screen
car.carNode.physicsBody?.applyForce(CGVector(dx: 100, dy: 0))
}
// Gas Button
if (gas.gasButton.contains(location)) {
touching = true
}
// Brake Button
else if (brake.brakeButton.contains(location)) {
car.carNode.physicsBody?.friction = 1
}
}
}

Integration of the comment
for touch in touches {
let location = touch.location(in: self)
let velY = car.carNode.physicsBody?.velocity.dy
let factor:CGFloat = 100
let maxFactor:CGFloat = 300
//limit
let dxCalc = factor * velY > maxFactor ? maxFactor : factor * velY
if location.x < self.frame.size.width / 2 {
// Left side of the screen
car.carNode.physicsBody?.applyForce(CGVector(dx: -dxCalc, dy: 0))
} else {
// Right side of the screen
car.carNode.physicsBody?.applyForce(CGVector(dx: dxCalc, dy: 0))
}
// Gas Button
//etc
}
}

Related

why when ever I try to tap on my SKSpriteNode it is not interactive

I am trying to have my SKSpriteNode named "slots" be tapped on when ever a user is trying to hit a target in the middle of the sprite. But whenever I Tapp on the slot in does not blink or do anything really.
I was trying to have it blink and print("Tapped on shotSlotNode") before I'll go on to continue creating this game but im still struck on this part of the game.
var slots = [shotSlot]()
var targets = [shotSlot]()
var gameScore: SKLabelNode!
var slotsRed = [targetSlotRed]()
var shotSlotNode: shotSlot? // Define a property for shotSlotNode
override func didMove(to view: SKView) {
let backGround = SKSpriteNode(imageNamed: "background")
backGround.position = CGPoint(x: 512, y: 384)
backGround.blendMode = .replace
backGround.zPosition = -1
// line 20 makes sure things go on top of the background view.
backGround.scale(to: CGSizeMake(1024, 768))
// I used line 21 to strech the image out to fix my scrrens size because I know I set my screen size to 1024 in the GameScene UI
addChild(backGround)
gameScore = SKLabelNode(fontNamed: "Chalkduster")
gameScore.text = "Score: 0"
gameScore.position = CGPoint(x: 480, y: 70)
addChild(gameScore)
self.view?.isMultipleTouchEnabled = true
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 560)) }
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 370)) }
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 200)) }
// this code breaks my slots into 3 rows and 3 cloumms of the shotSlots.
for i in 0 ..< 1 { createGreenTarget(at: CGPoint(x: 120 + (i * 370), y: 560)) }
for i in 0 ..< 1 { createRedTarget(at: CGPoint(x: 860 + (i * 370), y: 200)) }
}
func slotTapped(_ slot: shotSlot) {
// Make the slot sprite blink
print("Tapped on slot")
let blinkOut = SKAction.fadeAlpha(to: 0.2, duration: 0.15)
let blinkIn = SKAction.fadeAlpha(to: 1, duration: 0.15)
let blink = SKAction.sequence([blinkOut, blinkIn])
let blinkForever = SKAction.repeatForever(blink)
slot.sprite.run(blinkForever)
print("Started blinking")
// Handle the slot tap logic
if slot === shotSlotNode {
print("Tapped on shot slot node")
}
}
// now we need to make this greenTarget slide.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
for slot in slots {
if slot.contains(location) {
// The touch is inside the slot node
print("Tapped on shot slot")
slotTapped(slot)
// You can also check if the tapped slot is your `shotSlotNode`
if slot === shotSlotNode {
print("Tapped on shot slot node")
}
}
}
}
}
func createSlot(at position: CGPoint) {
let slot = shotSlot()
slot.configure(at: position)
slot.zPosition = 1
slot.isUserInteractionEnabled = true
addChild(slot)
targets.append(slot) // Add the slot to the targets array
if position == CGPoint(x: 512, y: 100) {
slot.sprite.name = "shotSlotNode"
slot.isUserInteractionEnabled = true
self.shotSlotNode = slot
print("Set shotSlotNode to sprite with name \(slot.sprite.name)")
}
}
class shotSlot: SKNode {
let sprite = SKSpriteNode(imageNamed: "slots")
func configure(at position: CGPoint) {
self.position = position
sprite.name = "shotSlotNode"
// sprite.position = CGPoint(x: frame.midX, y: frame.midY)
sprite.scale(to: CGSizeMake(220, 140))
sprite.isUserInteractionEnabled = true
sprite.zPosition = 13
addChild(sprite)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if let node = self.childNode(withName: "shotSlotNode") {
let convertedLocation = node.convert(location, from: self)
if node.contains(convertedLocation) {
print("Tapped on shotSlotNode")
}
}
}
}
}

SpriteKit keep moving player in current direction while falling after touchesEnded

I'm making my own Mario Bros. replica for the first level to learn how to make games with iOS, with my own assets. So far I've managed to place three SKSpriteNodes for the controls (left, right, up), and my player node can move in those three directions, but if I make my player jump while running in either direction, as soon as I remove my finger from the "left control", the player loses all its momentum and falls right there (as if it hit a wall) instead of following the parabola.
I don't know what might be needed in this case to be an MRE, so this is basically the whole thing that can reproduce the issue, along with some attempts I've made to make it work.
Basically I tried to apply an impulse / set the velocity / change the position directly and this last one was the one with better results (yet it still makes the player node to fall as soon as I remove the finger from the direction controls).
Here's a video demonstrating the issue.
This is the GameScene
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var player = SKSpriteNode()
private var bg = SKSpriteNode()
private var leftArrow = SKSpriteNode()
private var rightArrow = SKSpriteNode()
private var upArrow = SKSpriteNode()
private var floor = [SKSpriteNode]()
private var isLeftTouched = false
private var isRightTouched = false
private var selectedNodes: [UITouch:SKSpriteNode] = [:]
override func didMove(to view: SKView) {
addBackground()
addFloor()
addPlayer(xOffset: 0, yOffset: 0)
addControls()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 50))
let touch = touches.first! as UITouch
let positionInScene = touch.location(in: self)
let touchedNode = self.atPoint(positionInScene)
for touch in touches {
let location = touch.location(in:self)
if let node = self.atPoint(location) as? SKSpriteNode {
if let name = touchedNode.name {
selectedNodes[touch] = node
if name == "up" {
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
} else if name == "left" {
isLeftTouched = true
} else if name == "right" {
isRightTouched = true
}
}
}
}
if let name = touchedNode.name {
if name == "up" {
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
} else if name == "left" {
isLeftTouched = true
} else if name == "right" {
isRightTouched = true
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//let direction = ((touches.first?.location(in: self).x)! < (touches.first?.previousLocation(in: self).x)!) ? Direction.LEFT : Direction.RIGHT
//runIn(direction: direction)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if selectedNodes[touch] != nil {
if selectedNodes[touch]?.name == "left" {
isLeftTouched = false
} else if selectedNodes[touch]?.name == "right" {
isRightTouched = false
}
selectedNodes[touch] = nil
}
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if isLeftTouched {
runIn(direction: Direction.LEFT)
}
if isRightTouched {
runIn(direction: Direction.RIGHT)
}
}
// MARK: INTERACTION METHODS
func runIn(direction: Direction) {
let x = player.position.x + (direction == Direction.RIGHT ? 5 : -5)
let position = CGPoint(x: x, y: player.position.y)
if position.x >= self.frame.maxX || position.x <= self.frame.minX {
return
}
player.position = position
//player.physicsBody?.velocity = CGVector(dx: direction == Direction.RIGHT ? 50 : -50, dy: 0)
//player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 5 : -5 , dy: 0))
}
// MARK: UI METHODS
func addBackground() {
let bgTexture = SKTexture(imageNamed: "bg")
bg = SKSpriteNode(texture: bgTexture)
bg.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
bg.size.height = self.frame.height
bg.zPosition = -10
self.addChild(bg)
}
func addPlayer(xOffset: CGFloat, yOffset: CGFloat) {
let playerTexture = SKTexture(imageNamed: "player")
player = SKSpriteNode(texture: playerTexture)
//let xPos = calculateXOffset(for: player, from: self.frame.midX, offset: xOffset)
//let yPos = calculateXOffset(for: player, from: self.frame.midY, offset: yOffset)
player.position = CGPoint(x: self.frame.midX,
y: self.frame.midY)
player.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: player.frame.width, height: player.frame.height))
player.physicsBody?.isDynamic = true
self.addChild(player)
}
func addFloor() {
let blockTexture = SKTexture(imageNamed: "block")
for i in 0 ... (Int) (self.frame.width / blockTexture.size().width) {
let blockNode = SKSpriteNode(texture: blockTexture)
blockNode.position = CGPoint(x: self.frame.minX + (blockNode.frame.width * CGFloat(i)),
y: self.frame.minY + blockNode.frame.height / 2)
blockNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: blockNode.frame.width, height: blockNode.frame.height))
blockNode.physicsBody?.isDynamic = false
floor.append(blockNode)
self.addChild(blockNode)
}
}
func addControls() {
addLeftArrow()
addRightArrow()
addUpArrow()
}
func addLeftArrow() {
let leftTexture = SKTexture(imageNamed: "left")
leftArrow = SKSpriteNode(texture: leftTexture)
leftArrow.name = "left"
leftArrow.position = CGPoint(x: calculateXOffset(for: leftArrow, from: self.frame.minX, offset: 50),
y: calculateXOffset(for: leftArrow, from: self.frame.minY, offset: 50))
self.addChild(leftArrow)
}
func addRightArrow() {
let rightTexture = SKTexture(imageNamed: "right")
rightArrow = SKSpriteNode(texture: rightTexture)
rightArrow.name = "right"
rightArrow.position = CGPoint(x: calculateXOffset(for: rightArrow, from: self.frame.minX, offset: 150),
y: calculateXOffset(for: rightArrow, from: self.frame.minY, offset: 50))
self.addChild(rightArrow)
}
func addUpArrow() {
let upTexture = SKTexture(imageNamed: "up")
upArrow = SKSpriteNode(texture: upTexture)
upArrow.name = "up"
upArrow.position = CGPoint(x: calculateXOffset(for: upArrow, from: self.frame.maxX, offset: -(125 + upTexture.size().width)),
y: calculateXOffset(for: upArrow, from: self.frame.minY, offset: 50))
self.addChild(upArrow)
}
// MARK: UTILITY FUNCTIONS
func calculateXOffset(for asset: SKSpriteNode, from coord: CGFloat, offset: CGFloat) -> CGFloat {
let width = asset.frame.width
return coord + offset + width;
}
func calculateYOffset(for asset: SKSpriteNode, from coord: CGFloat, offset: CGFloat) -> CGFloat {
let height = asset.frame.height
return coord + offset + height;
}
}
My Direction enum:
enum Direction {
case LEFT
case RIGHT
case UP
case DOWN
}
And the only change I made in GameViewController was this:
scene.scaleMode = .resizeFill
My GameScene.sks is 926 x 428, only supporting landscape. I also set the LaunchScreen to Main due to a bug in Xcode 12: Background is not filling the whole view SpriteKit
And these are all my assets:
Edit
I tried applying an impulse in my runIn method like this:
player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 , dy: 0))
This makes the player node move in the parabola but now from time to time it gets stuck and the only way to make it move is to make it jump until it happens again.
Here's a video demonstrating the issue again.
If I try to set the velocity instead, then I'm not able to jump while moving and it seems to glide when jumping and moving after.
I ended up following #JohnL suggestion in the comments above, to use an impulse as well to move my player node:
player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 , dy: 0))
The issue where the player node was stuck while moving was removed when changing the floor for a single asset rather than multiple blocks one next to each other.

Moving SKSpriteNode in a semi circle with a virtual joystick

I am making a game that contains a virtual joystick. My joystick is composed by two SKSpriteNode, one for the stick the other for the base panel. ( see the picture below )
Obviously my joystick can only be moved following the green semi-circle on the panel.
My problem here is that i'm trying to make it happen, here is my code for now:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.location(in: self)
guard isTracking else {
return
}
let maxDistantion = panel.radius
let realDistantion = sqrt(pow(location.x, 2) + pow(location.y, 2))
let needPosition = realDistantion <= maxDistantion ? CGPoint(x: location.x, y: location.y) : CGPoint(x: location.x / realDistantion * maxDistantion, y: location.y / realDistantion * maxDistantion)
if (needPosition.y > 0) {
stick.position = needPosition
} else {
stick.position = CGPoint(x: needPosition.x, y: 0)
}
data = AnalogJoystickData(velocity: needPosition, angular: -atan2(needPosition.x, needPosition.y))
}
}
With this code, I can move into the semi-circle, but I would to only move my stick onto the green line, if someone can help me with my poor mathematics
Thank you

How do I move one node from one position to another position?

I have a file where I move a square from one side to a point near the middle of the screen.
After that, I want to move it to another position, but I am unsure of how to do that.
This is my code so far. When you run it, it moves to one point and then stops.
What I want it to do is it moves to that one point and immediately goes to a point with the same x value but a y value touching the top of the screen.
override func didMoveToView(view: SKView) {
self.physicsWorld.contactDelegate = self
createEnemies()
}
deinit{
print("deinit called")
}
func randomBetweenNumbers(firstNum: CGFloat, secondNum: CGFloat) -> CGFloat{
return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(firstNum - secondNum) + min(firstNum, secondNum)
}
//Helper method for spawning a point along the screen borders. This will not work for diagonal lines.
func randomPointBetween(start:CGPoint, end:CGPoint)->CGPoint{
return CGPoint(x: randomBetweenNumbers(start.x, secondNum: end.x), y: randomBetweenNumbers(start.y, secondNum: end.y))
}
func createEnemies(){
//Randomize spawning time.
//This will create a node every 0.5 +/- 0.1 seconds, means between 0.4 and 0.6 sec
let wait = SKAction .waitForDuration(0.5, withRange: 0.2)
weak var weakSelf = self //Use weakSelf to break a possible strong reference cycle
let spawn = SKAction.runBlock({
var random = arc4random() % 4 + 1
var position = CGPoint()
var moveTo = CGPoint()
var offset:CGFloat = 40
switch random {
//Left
case 1:
position = weakSelf!.randomPointBetween(CGPoint(x: 0, y: weakSelf!.frame.height/2), end: CGPoint(x: 0, y: weakSelf!.frame.height/2))
//Move to opposite side
moveTo = CGPoint(x: weakSelf!.frame.width/4, y: weakSelf!.frame.height/2)
//moveTo2 = CGPoint(x: weakSelf!.frame.width/4, y: weakSelf!.frame.height)
break
default:
break
}
weakSelf!.spawnEnemyAtPosition(position, moveTo: moveTo)
})
let spawning = SKAction.sequence([wait,spawn])
self.runAction(SKAction.repeatActionForever(spawning), withKey:"spawning")
}
func spawnEnemyAtPosition(position:CGPoint, moveTo:CGPoint){
let enemy = SKSpriteNode(color: SKColor.redColor(), size: CGSize(width: 40, height: 40))
enemy.position = position
enemy.physicsBody = SKPhysicsBody(rectangleOfSize: enemy.size)
enemy.physicsBody?.affectedByGravity = false
enemy.physicsBody?.dynamic = true
enemy.physicsBody?.collisionBitMask = 0 // no collisions
//Here you can randomize the value of duration parameter to change the speed of a node
let move = SKAction.moveTo(moveTo,duration: 2.5)
//let remove = SKAction.removeFromParent()
enemy.runAction(SKAction.sequence([move]))
/*if enemy.position = CGPoint(x: weakSelf!.frame.width/4, y: weakSelf!.frame.height/2) {
let move2 = SKAction.moveTo(moveTo2,duration: 2.5)
enemy.runAction(SKAction.sequence([move2]))
}*/
self.addChild(enemy)
print("\(enemy.position)")
}
func didBeginContact(contact: SKPhysicsContact) {
}
/*
Added for debugging purposes
override func touchesBegan(touches: NSSet, withEvent event: UIEvent?) {
//Just make a transition to the other scene, in order to check if deinit is called
//You have to make a new scene ... I named it WelcomeScene
var scene:WelcomeScene = WelcomeScene(fileNamed: "WelcomeScene.sks")
scene.scaleMode = .AspectFill
self.view?.presentScene(scene )
}
*/
Run a sequence for example.
moveTo = CGPoint(x: weakSelf!.frame.width/4, y: weakSelf!.frame.height/2)
moveTo2 = CGPoint(x: weakSelf!.frame.width/4, y: weakSelf!.frame.height)
let move = SKAction.moveTo(moveTo,duration: 2.5)
let move2 = SKAction.moveTo(moveTo2,duration: 2.5)
let moveToSequence = SKAction.sequence([move, move2])
enemy.runAction(moveToSequence)

Ios Simulator in Xcode 6 running at 0.0 fps

I know other people have asked questions about slow ios simulators before. But my problem is not the the simulator is slow. It is that the simulator runs at 0.0 fps (according to the text at the bottom) and I can't test any of my code. I am very confused and any help would be appreciated. Don't have enough reputation to post an image. But at the bottom it says 3 nodes 0.0 fps.
UPDATE
I deleted the following code from my code and it started working like normal again.
while left {
var leftMove = SKAction.moveByX(1, y: 0, duration: 0.1)
person.runAction(leftMove)
}
while !left{
var rightMove = SKAction.moveByX(-1, y: 0, duration: 0.1)
person.runAction(rightMove)
}
that was taken from my update() method
here is the whole thing...
import SpriteKit
class GameScene: SKScene {
var person = SKSpriteNode(imageNamed: "guyLeft_1.png")
var left = true
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.whiteColor()
var ground = SKShapeNode(rectOfSize: CGSizeMake(self.frame.size.width, self.frame.size.height * 0.2))
ground.position = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height * 0.1)
ground.fillColor = SKColor.blueColor()
self.addChild(ground)
person.position = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height * 0.2)
self.addChild(person)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
!left
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
while left {
var leftMove = SKAction.moveByX(1, y: 0, duration: 0.1)
person.runAction(leftMove)
}
while !left{
var rightMove = SKAction.moveByX(-1, y: 0, duration: 0.1)
person.runAction(rightMove)
}
}
}
It says 0fps because it is not able to render the scene just once.
Just change it to
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if left {
var leftMove = SKAction.moveByX(1, y: 0, duration: 0.1)
person.runAction(leftMove)
}
if !left { // or use an if-else construct
var rightMove = SKAction.moveByX(-1, y: 0, duration: 0.1)
person.runAction(rightMove)
}
}
There is really not much to explain here - the while just caused an infinite loop. If you have no break statements and the while body does not alter the evaluated condition left the program will be stuck forever inside the while loop.
Additionally:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
left = false // either
left = !left // or
}
The previous code just evaluates !left and ignores the result - effectively does nothing.

Resources