GameplayKit GKGoal: can't get wandering to work - swift

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
}
}

Related

Core Graphics with DisplayLink Unexpected Behavior

I'm trying to learn Core Graphics and am having trouble understanding the behavior of the code I've written, which uses a subclassed UIView and an override of the draw(_ rect:) function.
I've written a basic bouncing ball demo. Any number of random balls are created with random position and speed. They then bounce around the screen.
My issue is the way that the balls appear to move is unexpected and there is a lot of flicker. Here is the sequence inside for loops to iterate through all balls:
Check for collisions.
If there is a collision with the wall, multiply speed by -1.
Increment ball position by ball speed.
I'm currently not clearing the context, so I would expect the existing balls to stay put. Instead they seem to slide smoothly along with the ball that's moving.
I'd like to understand why this is the case.
Here is an image of how the current code runs at 4 fps so that you can see how the shapes are being drawn and shift back and forth:
Here is my code:
class ViewController: UIViewController {
let myView = MyView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
NSLayoutConstraint.activate([
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
myView.widthAnchor.constraint(equalTo: view.widthAnchor),
myView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
createDisplayLink(fps: 60)
}
func createDisplayLink(fps: Int) {
let displaylink = CADisplayLink(target: self,
selector: #selector(step))
displaylink.preferredFramesPerSecond = fps
displaylink.add(to: .current,
forMode: RunLoop.Mode.default)
}
#objc func step(displaylink: CADisplayLink) {
myView.setNeedsDisplay()
}
}
class MyView: UIView {
let numBalls = 5
var balls = [Ball]()
override init(frame:CGRect) {
super.init(frame:frame)
for _ in 0..<numBalls {
balls.append(
Ball(
ballPosition: Vec2(x: CGFloat.random(in: 0...UIScreen.main.bounds.width), y: CGFloat.random(in: 0...UIScreen.main.bounds.height)),
ballSpeed: Vec2(x: CGFloat.random(in: 0.5...2), y: CGFloat.random(in: 0.5...2))))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
for i in 0..<numBalls {
if balls[i].ballPosition.x > self.bounds.width - balls[i].ballSize || balls[i].ballPosition.x < 0 {
balls[i].ballSpeed.x *= -1
}
balls[i].ballPosition.x += balls[i].ballSpeed.x
if balls[i].ballPosition.y > self.bounds.height - balls[i].ballSize || balls[i].ballPosition.y < 0 {
balls[i].ballSpeed.y *= -1
}
balls[i].ballPosition.y += balls[i].ballSpeed.y
}
for i in 0..<numBalls {
context.setFillColor(UIColor.red.cgColor)
context.setStrokeColor(UIColor.green.cgColor)
context.setLineWidth(0)
let rectangle = CGRect(x: balls[i].ballPosition.x, y: balls[i].ballPosition.y, width: balls[i].ballSize, height: balls[i].ballSize)
context.addEllipse(in: rectangle)
context.drawPath(using: .fillStroke)
}
}
}
There are a lot of misunderstandings here, so I'll try to take them one by one:
CADisplayLink does not promise it will call your step method every 1/60 of a second. There's a reason the property is called preferred frames per second. It's just a hint to the system of what you'd like. It may call you less often, and in any case there will be some amount of error.
To perform your own animations by hand, you need to look at what time is actually attached to the given frame, and use that to determine where things are. The CADisplayLink includes a timestamp to let you know that. You can't just increment by speed. You need to multiply speed by actual time to determine distance.
"I'm currently not clearing the context, so I would expect the existing balls to stay put." Every time draw(rect:) is called, you receive a fresh context. It is your responsibility to draw everything for the current frame. There is no persistence between frames. (Core Animation generally provides those kinds of features by efficiently composing CALayers together; but you've chosen to use Core Graphics, and there you need to draw everything every time. We generally do not use Core Graphics this way.)
myView.setNeedsDisplay() does not mean "draw this frame right now." It means "the next time you're going to draw, this view needs to be redrawn." Depending on exactly when the CADisplayLink fires, you may drop a frame, or you might not. Using Core Graphics, you would need to update all the circle's locations before calling setNeedsDisplay(). Then draw(rect:) should just draw them, not compute what they are. (CADisplayLink is really designed to work with CALayers, though, and NSView drawing isn't designed to be updated so often, so this still may be a little tricky to keep smooth.)
The more normal way to create this system would be to generate a CAShapeLayer for each ball and position them on the NSView's layer. Then in the CADisplayLink callback, you would adjust their positions based on the timestamp of the next frame. Alternately, you could just set up a repeating NSTimer or DispatchTimerSource (rather than a CADisplayLink) at something well below the screen refresh speed (like 1/20 s) and move the layer positions in that callback. This would be nice and simple and avoid the complexities of CADisplayLink (which is much more powerful, but expects you to use the timestamp and consider other soft real-time concerns).

Entity-Component in 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.

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!

Swift Sprite Kit Add the amount of sprites per level

I don't have any code, but I was just wondering how you would add sprites to your scene based on what level the game was at. For example: Level 1 would have 1 sprite, level 2 would have 2, level 3 would have 3....
I don't want to have to write out every single level as the game idea could go up to 100's of levels.
I have a very simple idea in mind, but I can't seem to figure it out.
I'm sorry if this is too vague, but I don't have any real code to include. Any help would be greatly appreciated.
If each level is essentially the same, except the number sprites, this is what I would recommend:
1. Create a subclass of SKScene that holds logic common to all your levels. For example:
class Level : SKScene {
var numberOfSprites: Int = 0
private func createSprites() {
for _ in 0..<numberOfSprites {
let mySprite = SKSpriteNode(imageNamed: "YourSprite")
// Setup anything else with the sprite, like the position.
self.addChild(mySprite)
}
}
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
createSprites()
}
private func levelIsComplete() {
// Cell this when the level is complete to go to the next level.
if let view = self.view {
let nextLevel = Level(size: self.size)
nextLevel.numberOfSprites = self.numberOfSprites + 1
view.presentScene(nextLevel)
}
}
}
2. When creating your first level set the number of sprites to 1 and let it handle everything else.
let myLevel = Level(size: view.bounds.size)
myLevel.numberOfSprites = 1
Here's it in action
In my version I gave the sprites random positions and called levelIsComplete in touchesBegan.

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.