Entity-Component in Swift - swift

I am trying to build a simple iOS game using entity-component architecture similar to what is described here.
What I would like to achieve in my game is when a user touches the screen, detect where the touch occurred and move all entities of one type towards a specific direction (direction depends on where the user touched, right of screen = up, left of screen = down).
So far, the game is really simple and I am only getting started, but I am stuck in this simple functionality:
My issue is that an SKAction is supposed to run on all entities of a type, but happens at all.
Before I redesigned my game to an ECS approach, this worked fine.
Here is the GKEntity subclass that I declared in Lines.swift:
class Lines: GKEntity {
override init() {
super.init()
let LineSprite = SpriteComponent(color: UIColor.white, size: CGSize(width: 10.0, height: 300))
addComponent(LineSprite)
// Set physics body
if let sprite = component(ofType: SpriteComponent.self)?.node {
sprite.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: sprite.size.width, height: sprite.size.height))
sprite.physicsBody?.isDynamic = false
sprite.physicsBody?.restitution = 1.0
sprite.physicsBody?.friction = 0.0
sprite.physicsBody?.linearDamping = 0.0
sprite.physicsBody?.angularDamping = 0.0
sprite.physicsBody?.mass = 0.00
sprite.physicsBody?.affectedByGravity = false
sprite.physicsBody?.usesPreciseCollisionDetection = true
sprite.physicsBody?.categoryBitMask = 0b1
sprite.zPosition = 10
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In TouchesBegan I am calling the function Move(XAxisPoint: t.location(in: self)) which is declared in GameScene and here is what Move() does:
///Determines direction of movement based on touch location, calls MoveUpOrDown for movement
func move(XAxisPoint: CGPoint){
let Direction: SKAction
let Key: String
if XAxisPoint.x >= 0 {
Direction = SKAction.moveBy(x: 0, y: 3, duration: 0.01)
Key = "MovingUp"
} else {
Direction = SKAction.moveBy(x: 0, y: -3, duration: 0.01)
Key = "MovingDown"
}
moveUpOrDown(ActionDirection: Direction, ActionKey: Key)
}
///Moves sprite on touch
func moveUpOrDown(ActionDirection: SKAction, ActionKey: String) {
let Line = Lines()
if let sprite = Line.component(ofType: SpriteComponent.self)?.node {
if sprite.action(forKey: ActionKey) == nil {
stopMoving()
let repeatAction = SKAction.repeatForever(ActionDirection)
sprite.run(repeatAction, withKey: ActionKey)
}
}
}
///Stops movement
func stopMoving() {
let Line = Lines()
if let sprite = Line.component(ofType: SpriteComponent.self)?.node {
sprite.removeAllActions()
}
}
I am guessing there is some issue with this line of code Line.component(ofType: SpriteComponent.self)?.node but the compiler doesn't throw any errors and I am not sure where my mistake is.
Any help/guidance will be greatly appreciated!

The issue is the following line in MoveUpOrDown and StopMoving
let Line = Lines()
It's creating a new Lines object then telling it to run an action. Since it's new, it hasn't been added to the scene so it isn't drawn or acted on.
You should be getting an existing Lines object and modifying that instead of creating a new one.
As a side note, the common convention for naming methods and variables is to use camelCase which means MoveUpOrDown should be moveUpOrDown. On the other hand SnakeCase is used For classes structs and protocols so SpriteComponent is current. That allows you to know at a glance whether your working with a type or a variable.

Related

How can I handle a touch as a consequence of a previous touch? (Swift, SpriteKit)

This is a chess game in Swift using SpriteKit.
What I would like to happen is for the touched piece to move to the square touched on the next touch. When I run, the piece does not move when I try this. I know for certain that my code works up to and including 'case: "p"', and that the sprite is detecting the first touch.
In the image I've tried to implement this second touch using 'touch2', is this the correct thing to do? Thanks.
This is how I'm detecting the first touch and identifying the piece which has been touched. touchedPiece is the identified piece. moveset is an attribute determining the possible moves.
for touch in touches
{
let location = touch.location(in:self)
let firstTouchedNode = atPoint(location)
let touchedPiece:SKSpriteNode = (firstTouchedNode as? SKSpriteNode)!
switch touchedPiece.moveset {
case "p":
let locationToMove = touch.location(in:self)
let moveto = findpawnmoves(piece: touchedPiece)
spawnMoves(arr: moveto)
And here is the attempt to move the piece after locationToMove has been decided.
if moveto[0].contains(locationToMove){
move(piece: touchedPiece, location: moveto[0].position)
whiteMoved = true
deleteMoves(arr: moveto)
This is an example of how I am creating an array of possible squares to move to. I'll use the knight moves because it's the most concise.
func createMoveNode(piece: SKSpriteNode, up:Int, across:Int) -> SKSpriteNode{
let moveNode = SKSpriteNode(color: .clear, size: CGSize(width: CGFloat(cellsize), height: CGFloat(cellsize)))
moveNode.anchorPoint = CGPoint(x:0.5, y:0.5)
moveNode.position = CGPoint(x: piece.position.x + CGFloat(Double(across)*d), y: piece.position.y + CGFloat(Double(up)*d))
return moveNode
}
func findknightmoves(piece: SKSpriteNode) -> Array<SKSpriteNode>{
let move1 = createMoveNode(piece: piece, up:2, across:-1)
let move2 = createMoveNode(piece: piece, up:2, across:1)
let move3 = createMoveNode(piece: piece, up:-2, across:-1)
let move4 = createMoveNode(piece: piece, up:-2, across:1)
return [move1,move2,move3,move4]
}
func move(piece: SKSpriteNode, location: CGPoint){
piece.position = location
}
func spawnMoves(arr: Array<SKSpriteNode>){
for i in 0...arr.count-1{
arr[i].zPosition = 1
self.addChild(arr[i])
}
}
func deleteMoves(arr: Array<SKSpriteNode>){
for i in 0...arr.count-1{
arr[i].removeFromParent()
}
}

GameplayKit GKGoal: can't get wandering to work

Trying to learn how to use GameplayKit, and in particular, agents & behaviors. Trying to boil down all the tutorials and examples out there into a small, simple piece of code that I can use as a starting point for my own app. Unfortunately, what I've come up with doesn't work properly, and I can't figure out why. It's just a simple sprite with a simple GKGoal(toWander:). Rather than wandering, it just moves in a straight line to the right, forever. It also starts out impossibly slowly and speeds up impossibly slowly, despite my setting the max speed & acceleration to ridiculously high values. I can't figure out the fundamental difference between my simple code and all the complex examples out there. Here's the code, minus required init?(coder aDecoder: NSCoder):
class GremlinAgent: GKAgent2D {
override init() {
super.init()
maxAcceleration = 100000
maxSpeed = 1000000
radius = 20
}
override func update(deltaTime seconds: TimeInterval) {
super.update(deltaTime: seconds)
let goal = GKGoal(toWander: 100)
behavior = GKBehavior(goal: goal, weight: 1)
}
}
class Gremlin: GKEntity {
let sprite: SKShapeNode
init(scene: GameScene) {
sprite = SKShapeNode(circleOfRadius: 20)
sprite.fillColor = .blue
scene.addChild(sprite)
super.init()
let agent = GremlinAgent()
addComponent(agent)
let node = GKSKNodeComponent(node: sprite)
addComponent(node)
agent.delegate = node
}
}
And in GameScene.swift, didMove(to view:):
let gremlin = Gremlin(scene: self)
entities.append(gremlin)
Can anyone help me out?
As has been pointed out elsewhere, you have to set the weight for the goal very high. Try 100, or even 1000, and notice the difference in behavior. But even with these large weights, there's still a problem in your example: the maxSpeed value. You can't set it so high, or your sprite will just fly off in a straight line. Set it to a value closer to the speed you set in the GKGoal object.
Also notice that the wandering will always start off in the direction your sprite is pointing, so if you don't want it to always start off moving to the right, set zRotation to some random value.
Finally, don't create a new behavior in every call to update(). For wandering, you can just set it once, say, in init().
Here's some code that works:
class GremlinAgent: GKAgent2D {
override init() {
super.init()
maxAcceleration = 100000
maxSpeed = 100
let goal = GKGoal(toWander: 100)
behavior = GKBehavior(goal: goal, weight: 1000)
}
}
class Gremlin: GKEntity {
let sprite: SKShapeNode
init(scene: GameScene) {
sprite = SKShapeNode(circleOfRadius: 20)
sprite.fillColor = .blue
sprite.zRotation = CGFloat(GKRandomDistribution(lowestValue: 0, highestValue: 360).nextInt())
scene.addChild(sprite)
super.init()
let agent = GremlinAgent()
addComponent(agent)
let node = GKSKNodeComponent(node: sprite)
addComponent(node)
agent.delegate = node
}
}

Swift: Strange Collision Behavior

I'm building a Breakout game (#cs193p) and I've got the beginnings of the general working set up: The bricks, ball, and paddle all draw as they should. The collisions sort of work as they should, except the collision boundaries appear to be sort of wrong. They're generally larger than the paths I've constructed them with, but not always.
I've set the elasticity of the ball to zero, so that it rests on the paddle, so that the discrepancy is clear. This screenshot shows the ball resting on the incorrect collision boundary of the paddle.
The bricks and the black area at the bottom respond a little differently. For the bricks, the ball seems to be colliding with the bottom row of bricks when it visually reaches the next row. I've got the bricks disappearing "correctly" so the collision is doubly confirmed by the bricks disappearing. The black area at the bottom (the space for panning to move the paddle) has a similar issue: the ball dips a little into this area before bouncing.
Here is, well, a bunch of code, because I don't know where the problem might lie.
From my BreakoutBehavior class (I'm leaving out the gravity section because it doesn't seem to be a part of the problem):
let collider: UICollisionBehavior = {
let collider = UICollisionBehavior()
collider.translatesReferenceBoundsIntoBoundary = true
return collider
}()
private let ballBehavior: UIDynamicItemBehavior = {
let behavior = UIDynamicItemBehavior()
behavior.allowsRotation = true
behavior.elasticity = 1.25
return behavior
}()
internal func addColliderBoundary(path: UIBezierPath, named name: String) {
collider.removeBoundary(withIdentifier: name as NSCopying)
collider.addBoundary(withIdentifier: name as NSCopying, for: path)
}
override init() {
super.init()
addChildBehavior(gravity)
addChildBehavior(collider)
addChildBehavior(ballBehavior)
}
internal func addItem (_ item: UIDynamicItem) {
gravity.addItem(item)
collider.addItem(item)
ballBehavior.addItem(item)
}
And here's some code from my BreakoutView class:
I'm giving you the paddle as just one example of a boundary problem, rather than also giving the bricks and pan area, to be more efficient. So of course some of the variables I refer to won't be present in my excerpts. Note that I've left out some things like adding the behavior to the animator, but know that everything is indeed working, I'm just having these boundary problems.
private var paddleSize: CGSize {
return CGSize(width: brickSize.width * 3, height: brickSize.height)
}
private var paddleOrigin: CGPoint {
let x = frame.midX - paddleSize.width / 2
let y = panOrigin.y - paddleSize.height
return CGPoint(x: x, y: y)
}
private func addBallAndPaddle () {
let paddleFrame = CGRect(origin: paddleOrigin, size: paddleSize)
let paddleView = UIView(frame: paddleFrame)
paddleView.backgroundColor = UIColor.white
paddleView.layer.borderWidth = 0.25
addSubview(paddleView)
paddle = paddleView
behavior.addColliderBoundary(path: UIBezierPath(rect: paddleFrame), named: Boundaries.paddle)
let ballFrame = CGRect(origin: ballOrigin, size: ballSize)
let ballView = UIView(frame: ballFrame)
ballView.backgroundColor = UIColor.white
ballView.layer.borderWidth = 0.25
addSubview(ballView)
behavior.addItem(ballView)
ball = ballView
}
private lazy var animator: UIDynamicAnimator = {
let animator = UIDynamicAnimator(referenceView: self.superview!)
animator.delegate = self
return animator
}()
private lazy var behavior: BreakoutBehavior = {
let behavior = BreakoutBehavior()
behavior.collider.collisionDelegate = self
return behavior
}()
And here's code from the BreakoutViewController. configureGameView() is called in viewDidLayoutSubviews
private func configureGameView () {
gameView.frame = topView.frame // topView is the view in IB
gameView.backgroundColor = UIColor.lightGray
gameView.addViews() // adds bricks, ball, paddle, pan area
firstTap.numberOfTapsRequired = 1
gameView.addGestureRecognizer(firstTap) // DOESN'T WORK
if let panView = gameView.panner {
panView.addGestureRecognizer(UIPanGestureRecognizer(target: gameView, action: #selector(gameView.movePaddle(_:))))
panView.addGestureRecognizer(firstTap) // DOESN'T WORK
}
}
Thanks!

Inconsistent contact detection in Swift 3 using SpriteKit

I'm having an issue with contact detection in Swift 3 using SpriteKit. The contact detection is working...sometimes. It seems purely random as to when it fires and when it doesn't.
I have a yellow "bullet" that moves up on the screen to hit a red sprite named targetSprite. The desired behavior is to have the bullet removed when it hits the target, but sometimes it just passes through underneath.
I've found many questions about contact detection not working at all, but I haven't found any dealing with inconsistent detection.
What can I do to fix this?
Here's the code:
import SpriteKit
import GameplayKit
enum PhysicsCategory:UInt32 {
case bullet = 1
case sprite1 = 2
case targetSprite = 4
// each new value should double the previous
}
class GameScene: SKScene, SKPhysicsContactDelegate {
// Create sprites
let sprite1 = SKSpriteNode(color: SKColor.blue, size: CGSize(width:100,height:100))
let targetSprite = SKSpriteNode(color: SKColor.red, size: CGSize(width:100,height:100))
let bullet = SKSpriteNode(color: SKColor.yellow, size: CGSize(width: 20, height: 20))
// show the bullet?
var isShowingBullet = true
// Timers
//var timer:Timer? = nil
var fireBulletTimer:Timer? = nil
// set up bullet removal:
var bulletShouldBeRemoved = false
let bulletMask = PhysicsCategory.bullet.rawValue
override func didMove(to view: SKView) {
// Physics
targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.centerRect.size)
targetSprite.physicsBody?.affectedByGravity = false
bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.centerRect.size)
bullet.physicsBody?.affectedByGravity = false
// Contact Detection:
targetSprite.physicsBody?.categoryBitMask = PhysicsCategory.targetSprite.rawValue
targetSprite.physicsBody?.contactTestBitMask =
//PhysicsCategory.sprite1.rawValue |
PhysicsCategory.bullet.rawValue
targetSprite.physicsBody?.collisionBitMask = 0 // no collision detection
// bullet physics
bullet.physicsBody?.categoryBitMask = PhysicsCategory.bullet.rawValue
bullet.physicsBody?.contactTestBitMask =
PhysicsCategory.targetSprite.rawValue
bullet.physicsBody?.collisionBitMask = 0 // no collision detection
// execute once:
fireBulletTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(self.fireBullet),
userInfo: nil,
repeats: false)
// Add sprites to the scene:
self.addChild(sprite1)
self.addChild(bullet)
self.addChild(targetSprite)
// Positioning
targetSprite.position = CGPoint(x:0, y:300)
// Note: bullet and sprite1 are at 0,0 by default
// Delegate
self.physicsWorld.contactDelegate = self
}
func didBegin(_ contact: SKPhysicsContact) {
print("didBegin(contact:))")
//let firstBody:SKPhysicsBody
// let otherBody:SKPhysicsBody
// Use 'bitwise and' to see if both bits are 1:
if contact.bodyA.categoryBitMask & bulletMask > 0 {
//firstBody = contact.bodyA
//otherBody = contact.bodyB
print("if contact.bodyA....")
bulletShouldBeRemoved = true
}
else {
//firstBody = contact.bodyB
//otherBody = contact.bodyA
print("else - if not contacted?")
}
/*
// Find the type of contact:
switch otherBody.categoryBitMask {
case PhysicsCategory.targetSprite.rawValue: print(" targetSprite hit")
case PhysicsCategory.sprite1.rawValue: print(" sprite1 hit")
case PhysicsCategory.bullet.rawValue: print(" bullet hit")
default: print(" Contact with no game logic")
}
*/
} // end didBegin()
func didEnd(_ contact: SKPhysicsContact) {
print("didEnd()")
}
func fireBullet() {
let fireBulletAction = SKAction.move(to: CGPoint(x:0,y:500), duration: 1)
bullet.run(fireBulletAction)
}
func showBullet() {
// Toggle to display or not, every 1 second:
if isShowingBullet == true {
// remove (hide) it:
bullet.removeFromParent()
// set up the toggle for the next call:
isShowingBullet = false
// debug:
print("if")
}
else {
// show it again:
self.addChild(bullet)
// set up the toggle for the next call:
isShowingBullet = true
// debug:
print("else")
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if bulletShouldBeRemoved {
bullet.removeFromParent()
}
}
}
Sorry for the inconsistent indentation, I can't seem to find an easy way to do this...
EDIT:
I have found that using 'frame' instead of 'centerRect' makes the collision area the size of the sprite. For example:
targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.centerRect.size)
should be:
targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.frame.size)
First advice - Do not use NSTimer (aka Timer) in SpriteKit. It is not paired with a game loop and can cause different issues in a different situations. Read more here ( answer posted by LearnCocos2D)
So, do this:
let wait = SKAction.wait(forDuration: 1)
run(wait, completion: {
[unowned self] in
self.fireBullet()
})
What I have noticed is that if I run your code in Simulator, I get the behaviour you have described. didBegin(contact:) is being fired randomly. Still, this is not happening on a device for me, and device testing is what matters.
Now, when I have removed Timer and did the same thing with SKAction(s) everything worked, means contact were detected every time.
Have you tried adding
.physicsBody?.isDynamic = true
.physicsBody?.usesPreciseCollisionDetrction =true
SpriteKit physics engine will calculate collision correctly if you do following:
1) set "usesPreciseCollisionDetection" property to true for bullet's physics body. This will change collision detection algorithm for this body. You can found more information about this property here, chapter "Working with Collisions and Contacts".
2) move your bullet using applyImpulse or applyForce methods. Collision detection will not woking correctly if you move body by changing it's position manually. You can find more information here, chapter "Making Physics Bodies Move".

Get radius of a SCNSphere that is used to create a SCNNode

How do i return the radius of a sphere geometry(SCNSphere) used to set up a SCNNode.
I want to use the radius in a method where i move some child nodes in relation to a parent node. The code below fails since radius appears to be unknown to the resulting node, should i not pass the node to the method?
Also my array index fails saying that Int is not a Range.
I am trying to build something from this
import UIKit
import SceneKit
class PrimitivesScene: SCNScene {
override init() {
super.init()
self.addSpheres();
}
func addSpheres() {
let sphereGeometry = SCNSphere(radius: 1.0)
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
let sphereNode = SCNNode(geometry: sphereGeometry)
self.rootNode.addChildNode(sphereNode)
let secondSphereGeometry = SCNSphere(radius: 0.5)
secondSphereGeometry.firstMaterial?.diffuse.contents = UIColor.greenColor()
let secondSphereNode = SCNNode(geometry: secondSphereGeometry)
secondSphereNode.position = SCNVector3(x: 0, y: 1.25, z: 0.0)
self.rootNode.addChildNode(secondSphereNode)
self.attachChildrenWithAngle(sphereNode, children:[secondSphereNode, sphereNode], angle:20)
}
func attachChildrenWithAngle(parent: SCNNode, children:[SCNNode], angle:Int) {
let parentRadius = parent.geometry.radius //This fails cause geometry does not know radius.
for var index = 0; index < 3; ++index{
children[index].position=SCNVector3(x:Float(index),y:parentRadius+children[index].radius/2, z:0);// fails saying int is not convertible to range.
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The issue with radius is that parent.geometry returns a SCNGeometry and not an SCNSphere. If you need to get the radius, you'll need to cast parent.geometry to SCNSphere first. To be safe, it's probably best to use some optional binding and chaining to do that:
if let parentRadius = (parent.geometry as? SCNSphere)?.radius {
// use parentRadius here
}
You'll also need to do that when accessing the radius on your children nodes. If you put all that together and clean things up a little, you get something like this:
func attachChildrenWithAngle(parent: SCNNode, children:[SCNNode], angle:Int) {
if let parentRadius = (parent.geometry as? SCNSphere)?.radius {
for var index = 0; index < 3; ++index{
let child = children[index]
if let childRadius = (child.geometry as? SCNSphere)?.radius {
let radius = parentRadius + childRadius / 2.0
child.position = SCNVector3(x:CGFloat(index), y:radius, z:0.0);
}
}
}
}
Note though that you're calling attachChildrenWithAngle with an array of 2 children:
self.attachChildrenWithAngle(sphereNode, children:[secondSphereNode, sphereNode], angle:20)
If you do that, you're going to get a runtime crash in that for loop when accessing the 3rd element. You'll either need to pass an array with 3 children every time you call that function, or change the logic in that for loop.
At this point I'm only working with spheres, hence the radius is quite easy to manipulate. But still, I think you should try to look at the geometry of the SCNNode. I use this function besides the code snippet you posted (the two works together just fine for me).
func updateRadiusOfNode(_ node: SCNNode, to radius: CGFloat) {
if let sphere = (node.geometry as? SCNSphere) {
if (sphere.radius != radius) {
sphere.radius = radius
}
}
}
Also the scaling factor (at the radius) is also important. I use a baseRadius then i'm multiplying that with
maxDistanceObserved / 2
Always updating the max distance if a hitTest has futher results.