Background: I'm working on a simple game/application in SpriteKit (Swift). It's similar to Air Hockey but with magnets (like tabletop game called Klask). I have already spent a lot of time adding a store, ads, menu, etc. It's been a fun learning project for me so far.
Current method: I am applying an impulse to a ball when the player/mallet strikes the ball. This impulse (which takes in a CGVector(dx:.., dy:..)) is calculated by the touch movement/position frame to frame. So for instance, if the player moved the mallet in the north east direction and a collision between the ball and the player is detected, the ball will move in that northeast direct (up and to the right).
Issue: This doesn't take into account when the player clips the ball, which causes a warpy effect.The player would expect the ball to move in a direction based on where the ball strikes on the players mallet (a round circle) rather than the direction of the touch (which does usually work pretty well).
Desired implementation (demo from acceleroto's Air Hockey):
Current implementation/code:
// In player class
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
{
bottomTouchIsActive = true
var releventTouch:UITouch!
//convert set to known type
let touchSet = touches
//get array of touches so we can loop through them
let orderedTouches = Array(touchSet)
for touch in orderedTouches
{
//if we've not yet found a relevent touch
if releventTouch == nil
{
//look for a touch that is in the activeArea (Avoid touches by opponent)
if activeArea.contains(CGPoint(x: touch.location(in: parent!).x, y: touch.location(in: parent!).y + frame.height * 0.24))
{
isUserInteractionEnabled = true
releventTouch = touch
}
else
{
releventTouch = nil
}
}
}
if (releventTouch != nil) && lastTouchTimeStamp != nil
{
//get touch position and relocate player
let location = CGPoint(x: releventTouch!.location(in: parent!).x, y: releventTouch!.location(in: parent!).y + frame.height * 0.24)
position = location
//find old location and use pythagoras to determine length between both points
let oldLocation = CGPoint(x: releventTouch!.previousLocation(in: parent!).x, y: releventTouch!.previousLocation(in: parent!).y + frame.height * 0.24)
let xOffset = location.x - oldLocation.x
let yOffset = location.y - oldLocation.y
let vectorLength = sqrt(xOffset * xOffset + yOffset * yOffset)
let seconds = releventTouch.timestamp - lastTouchTimeStamp!
let velocity = 0.015 * Double(vectorLength) / seconds
//to calculate the vector, the velcity needs to be converted to a CGFloat
let velocityCGFloat = CGFloat(velocity)
// NSUserDefaults for more direct access in collision detection
let forceSaveDX = UserDefaults.standard
forceSaveDX.set(velocityCGFloat * xOffset / vectorLength, forKey: "BottomForceDX")
forceSaveDX.synchronize()
let forceSaveDY = UserDefaults.standard
forceSaveDY.set(velocityCGFloat * yOffset / vectorLength, forKey: "BottomForceDY")
forceSaveDY.synchronize()
//Only apply an impulse if the touch is active.
delegate?.bottomTouchIsActive(bottomTouchIsActive, fromBottomPlayer: self)
//update latest touch time for next calculation
lastTouchTimeStamp = releventTouch.timestamp
}
else if (releventTouch != nil) && lastTouchTimeStamp == nil
{
//get touch position and relocate player
let location = CGPoint(x: releventTouch!.location(in: parent!).x, y: releventTouch!.location(in: parent!).y + frame.height * 0.24)
position = location
//find old location and use pythagoras to determine length between both points
let oldLocation = CGPoint(x: releventTouch!.previousLocation(in: parent!).x, y: releventTouch!.previousLocation(in: parent!).y + frame.height * 0.24)
let xOffset = location.x - oldLocation.x
let yOffset = location.y - oldLocation.y
let vectorLength = sqrt(xOffset * xOffset + yOffset * yOffset)
//update latest touch time for next calculation
lastTouchTimeStamp = releventTouch.timestamp
}
}
// In main gameScene
func didBegin(_ contact: SKPhysicsContact)
{
if (contact.bodyA.categoryBitMask == BodyType.ball.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue) && (contact.bodyA.contactTestBitMask == 25 || contact.bodyB.contactTestBitMask == 25) && bottomTouchForCollision == true
{
ball!.physicsBody!.applyImpulse(CGVector(dx: CGFloat(UserDefaults.standard.float(forKey: "BottomForceDX")), dy: CGFloat(UserDefaults.standard.float(forKey: "BottomForceDY"))))
}
}
I apologize for the long question, it's hard to explain what the issue is. But essentially I just need to make the impulse be calculated using where the ball hits the player mallet rather than by the direction of the touch. If this can be done directly in the collision detection function, that would be even better as there would be no extra frame dedicated to calculating the touch direction. I've been stuck for a while.
I think it would be best to incorporate the speed of the touch and the position where the ball strikes the mallet to have the most realistic impulse. I just don't know how to calculate the vector this way. I'm open to all ideas!
Related
I am trying to create a small game where SpriteNode (aka Player) moves up vertically on constant speed. I want to use its angle for steering left or right. However, I am not able to move the Player properly using its angle.
Thank you for your time.
Here is the partial code I wrote:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.previousLocation(in: self)
if location.x < self.size.width / 2 && location.y < self.size.height / 2 {
// Turn Left
print("TURNING LEFT")
turn(left: true)
} else if location.x >= self.size.width / 2 && location.y < self.size.height / 2 {
// Turn Right
print("TURNING RIGHT")
turn(left: false)
} else if location.y > self.size.height / 2 {
// Go Up
print("GOING UP!")
move()
}
}
}
func turn(left: Bool) {
if left {
// Turn Left
let turnAction = SKAction.rotate(byAngle: 0.1, duration: 0.05)
let repeatAction = SKAction.repeatForever(turnAction)
player?.run(repeatAction)
} else {
// Turn Right
let turnAction = SKAction.rotate(byAngle: -0.1, duration: 0.05)
let repeatAction = SKAction.repeatForever(turnAction)
player?.run(repeatAction)
}
}
func move() {
// Move Up
let moveAction = SKAction.moveBy(x: 0, y: 15, duration: 0.5)
let repeatAction = SKAction.repeatForever(moveAction)
player?.run(repeatAction)
}
Using Trigonometry you can determine the sprite's x and y speed in either direction create an angle for the sprite to point towards. A great article that sums up how to do this can be found here.
If you simply wish to literally rotate the sprite it can be done by creating an SKAction for the rotation and running the action on the node.
// Create an action, duration can be changed from 0 so the user can see a smooth transition otherwise change will be instant.
SKAction *rotation = [SKAction rotateByAngle: M_PI/4.0 duration:0];
//Simply run the action.
[myNode runAction: rotation];
Thanks to #makertech81 link, I was able to write below code which works:
func move() {
// Move Up
let playerXPos = sin((player?.zRotation)!) * -playerSpeed
let moveAction = SKAction.moveBy(x: playerXPos, y: playerSpeed, duration: 0.5)
let repeatAction = SKAction.repeatForever(moveAction)
player?.run(repeatAction)
}
Basically, because I know the angle through zRotation and also I know how much Player will move toward Y, I was able to calculate its sin (which is X value). Therefore, moveAction will be properly calculated toward its destination.
Hope it helps.
I try to build 2D - top down game, and I have player (SKSpriteNode) he can move and rotate, and I want to shoot two bullets from him.
I use this code to shoot:
func setBullet(player: Player, bullet: Int)
{
let weaponPosition = scene!.convertPoint(player.weapon.position, fromNode: player)
var xPos, yPos: CGFloat!
let sinus = sin(player.zRotation)
let cosinus = cos(player.zRotation)
if bullet == 1
{
xPos = converted.x - sinus * player.size.height / 2
yPos = converted.y + cosinus * player.size.height / 2
}
else if bullet == 2
{
xPos = weaponPosition.x - sinus * player.size.height / 2
yPos = weaponPosition.y + cosinus * player.size.height / 2
}
position = CGPoint(x: xPos, y: yPos)
physicsBody!.applyImpulse(CGVector(dx: -sinus * normalSpeed, dy: cosinus * normalSpeed))
}
But, i do not know how to correctly set the position...
I try to make something like this
(Green dots - this is a bullets). Can anyone help me please!
Shooting multiple bullets in the same direction is fairly straightforward. The key is to determine the bullets' initial positions and direction vectors when the character is rotated.
You can calculate a bullet's initial position within the scene by
let point = node.convertPoint(weapon.position, toNode: self)
where node is the character, weapon.position is non-rotated position of a gun, and self is the scene.
Typically, a bullet moves to the right, CGVector(dx:1, dy:0), or up, CGVector (dx:0, dy:1), when the character is not rotated. You can calculate the direction of the impulse to apply to the bullet's physics body by rotating the vector by the character's zRotation with
func rotate(vector:CGVector, angle:CGFloat) -> CGVector {
let rotatedX = vector.dx * cos(angle) - vector.dy * sin(angle)
let rotatedY = vector.dx * sin(angle) + vector.dy * cos(angle)
return CGVector(dx: rotatedX, dy: rotatedY)
}
Here's an example of how to shoot two bullets from a rotated character:
struct Weapon {
var position:CGPoint
}
class GameScene: SKScene {
let sprite = SKSpriteNode(imageNamed:"Spaceship")
let dualGuns = [Weapon(position: CGPoint(x: -15, y: 15)), Weapon(position: CGPoint(x: 15, y: 15))]
let singleGun = [Weapon(position: CGPoint(x: 0, y: 15))]
let numGuns = 1
// If your character faces up where zRotation == 0, offset by pi
let rotationOffset = CGFloat(M_PI_2)
override func didMoveToView(view: SKView) {
scaleMode = .ResizeFill
sprite.position = view.center
sprite.size = CGSizeMake(25, 25)
self.addChild(sprite)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let _ = touches.first {
let action = SKAction.rotateByAngle(CGFloat(M_PI_4/2), duration:1)
sprite.runAction(action) {
[weak self] in
if let scene = self {
switch (scene.numGuns) {
case 1:
for weapon in scene.singleGun {
scene.shoot(weapon: weapon, from: scene.sprite)
}
case 2:
for weapon in scene.dualGuns {
scene.shoot(weapon: weapon, from: scene.sprite)
}
default:
break
}
}
}
}
}
func shoot(weapon weapon:Weapon, from node:SKNode) {
// Convert the position from the character's coordinate system to scene coodinates
let converted = node.convertPoint(weapon.position, toNode: self)
// Determine the direction of the bullet based on the character's rotation
let vector = rotate(CGVector(dx: 0.25, dy: 0), angle:node.zRotation+rotationOffset)
// Create a bullet with a physics body
let bullet = SKSpriteNode(color: SKColor.blueColor(), size: CGSizeMake(4,4))
bullet.physicsBody = SKPhysicsBody(circleOfRadius: 2)
bullet.physicsBody?.affectedByGravity = false
bullet.position = CGPointMake(converted.x, converted.y)
addChild(bullet)
bullet.physicsBody?.applyImpulse(vector)
}
// Rotates a point (or vector) about the z-axis
func rotate(vector:CGVector, angle:CGFloat) -> CGVector {
let rotatedX = vector.dx * cos(angle) - vector.dy * sin(angle)
let rotatedY = vector.dx * sin(angle) + vector.dy * cos(angle)
return CGVector(dx: rotatedX, dy: rotatedY)
}
}
Suppose your player is a circle maked with SKShapeNode or SKSpriteNode.
Both of them have the frame property:
let f = player.frame
So, the first bullet can be in this position:
let firstBullet.position = CGPointMake(player.position.x-(f.width/2),player.position.y)
let secondBullet.position = CGPointMake(player.position.x+(f.width/2),player.position.y)
To know it during rotation do:
let firstBulletXPos = firstBullet.position.x - sinus * bullet.size.height / 2
let firstBulletYPos = firstBullet.position.y + cosinus * bullet.size.height / 2
let secondBulletXPos = secondBullet.position.x - sinus * bullet.size.height / 2
let secondBulletYPos = secondBullet.position.y + cosinus * bullet.size.height / 2
I'm working in a game for my college and i stuck in one thing.
when i try to shoot with my hero the bullet go's up only or side i need my bullet go's where ever my hero rotate plz help me this is my code
func spownBullet() {
let bullet = SKSpriteNode(imageNamed: "Bullet")
let hero = self.childNodeWithName("hero") as! SKSpriteNode
bullet.zPosition = 1
bullet.position = CGPointMake(hero.position.x, hero.position.y)
bullet.size = CGSize(width: 20, height: 30)
let bulletact = SKAction.moveToY(self.size.height + 300, duration: 1)
bullet.runAction(SKAction.repeatActionForever(bulletact))
self.addChild(bullet)
}
If I understand correctly you want your bullet to go a certain distance (e.g. 300) in the same angle as your "hero" sprite is rotated.
Here's a function to compute the destination point:
// destination point moving at angle
// ---------------------------------
func endPointMovingSpriteFrom(origin:CGPoint, atAngle angle:CGFloat,forDistance distance:CGFloat ) -> CGPoint
{
let deltaX = distance / sin(angle + CGFloat(M_PI)/2)
let deltaY = distance / cos(angle + CGFloat(M_PI)/2)
return CGPoint(x: origin.x - deltaX, y:origin.y - deltaY)
}
You can use it to compute the destination and then use the destination in a moveTo action:
let bulletDestination = endPointMovingSpriteFrom(hero.position, atAngle:hero.zRotation, forDistance:300)
let bulletact = SKAction.moveTo(bulletDestination, duration: 1)
Details of program: by looking at this picture(http://i.stack.imgur.com/QOZ53.png), what I'm trying to do is have the spaceship circle the planet. I achieved this by making a line node and changes its anchor point to the top and then position it to the centre of the planet which then upon impact of the spaceship and planet, the line is angled towards the ship in which then the ship is removed from the view and added to the line node as a child and together they rotate around the planet, as the ship rotates around the the planet, the ship it self also rotates on the spot (hopefully that makes sense)
Problem: the problem is im not getting the correct zRotation value of the ship. Right now I got the code to draw another ship at the location where it was tapped and set the zRotation of that image to the zRotation of the ship but i keep getting different angles. (refer to picture). Is this because I have ship added to line node as a child? Im running a rotating animation on both the line and the ship. In the picture, the line is rotating around the planet counter clockwise dragging the ship along with it and the ship itself is also rotating counter clockwise on the spot. In the picture the left ship; the one that's touching the line is the one thats rotating, the ship on the right is just drawn to angle of the rotating ship but by looking at the picture you can see the angle of the ship is pretty opposite of the one on the left. Why is this so? what I noticed is that when the ship is at the bottom half of the planet, the angles are fine but when it comes to the top half, the angles are kinda opposite(refer to picture)
Picture
Console Log:
I'm Touched -
ship angle in degrees: 83.6418545381942
PLAYER POSITION X: 100.0 Y: 100.0
PLAYER ZROTATION: 1.45982575416565
WE'RE TOUCHING -
PLANET POSITION X*: 120.0 Y: 230.000015258789
PLAYER-SHIP POSITION X: 107.998710632324 Y: 171.783294677734
ANGLE OF LINE RADIANS*: -1.77409685090117 DEGRESS: -101.648262004087
PLAYER-SHIP ROTATION: 1.57079637050629
I'm Touched -
ship angle in degrees: 314.660859137531
TEMP POS X: 136.535125732422 Y: 287.094879150391
TEMP ZROTATION: 5.491868019104
PLAYER POSITION X: 136.535125732422 Y: 287.094879150391
PLAYER ZROTATION: 5.491868019104
Collision Code:
func didBeginContact(contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask == planetGroup || contact.bodyB.categoryBitMask == planetGroup {
print(" WE'RE TOUCHING ")
moving = true
touching = true
let degrees = 45.0
let radians = degrees * M_PI / 180.0
//rotate Line
var rotateLine = SKAction.rotateByAngle(CGFloat(radians), duration: 0.5)
var repeatLine = SKAction.repeatActionForever(rotateLine)
//rotates Ship
var rotateShip = SKAction.rotateByAngle(CGFloat(radians), duration: 0.4)
var repeatShip = SKAction.repeatActionForever(rotateShip)
playerShip.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
planetNode = contact.bodyA.node as! SKSpriteNode
planetX = planetNode.position.x
planetY = planetNode.position.y
playerX = playerShip.position.x
playerY = playerShip.position.y
var angleOfAnchor = AngleBetweenPoints(planetNode.position, endPoint: playerShip.position)
var three60 = 360 * CGFloat(M_PI) / 180.0
var nintey = 90 * CGFloat(M_PI) / 180.0
var inDegree = angleOfAnchor * 180.0 / CGFloat(M_PI)
var shipPlanetDistance = SDistanceBetweenPoints(planetNode.position, p2: playerShip.position)
line = SKSpriteNode(color: UIColor.blackColor(), size: CGSize(width: 2, height: planetNode.size.height))
line.anchorPoint = CGPoint(x: 0.5, y: 1)
line.position = CGPoint(x: planetX, y: planetY)
line.zRotation = -(three60 - nintey - angleOfAnchor)
self.addChild(line)
tempShip = playerShip
playerShip.removeFromParent()
line.runAction(repeatLine, withKey: "rotateLine")
//playerShip.position = CGPoint(x: playerX, y: playerY)
line.addChild(playerShip)
playerShip.zRotation = (90 * CGFloat(M_PI) / 180.0)
playerShip.runAction(repeatShip, withKey: "rotateShip")
print("*PLANET POSITION* X: \(planetX) Y: \(planetY) \r *PLAYER-SHIP POSITION* X: \(playerX) Y: \(playerY) \r *ANGLE OF LINE* RADIANS: \(angleOfAnchor) DEGRESS: \(inDegree) *PLAYER-SHIP ROTATION: \(playerShip.zRotation)")
}
}
Screen Touch Code:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
print(" I'm Touched ")
var radians:CGFloat = playerShip.zRotation
var degrees = radians * 180.0 / CGFloat(M_PI)
var dx = cos(radians)
var dy = sin(radians)
print(" ship angle in degrees: \(degrees) ")
var tempAngle = playerShip.zRotation
var shipPosition = convertPoint(playerShip.position, fromNode: line)
playerX = shipPosition.x
playerY = shipPosition.y
if startMove == true {
playerShip.removeActionForKey("rotateShip")
playerShip.physicsBody?.velocity = CGVector(dx: 100*dx, dy: 100*dy)// speed of direction
startMove = false
}
if moving == true{
//playerShip.removeActionForKey("rotateShip")
//playerShip.removeFromParent()
//self.addChild(playerShip)
var radians:CGFloat = playerShip.zRotation
var degrees = radians * 180.0 / CGFloat(M_PI)
var dx = cos(radians)
var dy = sin(radians)
print(" ship angle in degrees: \(degrees) ")
//playerShip.zRotation = tempShip.zRotation
//playerShip.position = CGPoint(x: playerX, y: playerY)
//playerShip.physicsBody?.velocity = CGVector(dx: 100*dx, dy: 100*dy)// speed of direction
// this is the ship that gets drawn
var temp = SKSpriteNode(imageNamed: "img/ship/2.png")
temp.position = CGPoint(x: playerX, y: playerY)
temp.zRotation = playerShip.zRotation
self.addChild(temp)
//moving = false
print("*TEMP POS* X: \(temp.position.x) Y: \(temp.position.y) *TEMP ZROTATION*: \(temp.zRotation)")
}
print("*PLAYER POSITION* X: \(playerX) Y: \(playerY) *PLAYER ZROTATION: \(playerShip.zRotation)")
}
I managed to solve this problem, I added the line.zrotation with the playerShip.zrotation and it ended up working perfectly
I'm creating a game, and I have a canon that shoots bullets.
I have already the code to change the orientation of the canon, :
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self {
if location.x < self.size.width / 2 {
let x = location.x
let y = location.y
let AB = self.size.width / 2 - x
let angleRadians = atan(AB/y)
let angleDegrees = angleRadians * (180 / CGFloat(M_PI))
canon.zRotation = angleDegrees * (CGFloat(M_PI) / 180)
}
if location.x > self.size.width / 2 {
let x = location.x
let y = location.y
let AB = x - self.size.width / 2
let angleRadians = atan(AB/y)
let angleDegrees = angleRadians * (180 / CGFloat(M_PI))
canon.zRotation = -(angleDegrees * (CGFloat(M_PI) / 180))
}
}
}
and I want to determine the direction of the bullet when the user shoots,
I tried with an action (moveBy) but I didn't find a relation between the orientation of the canon, and the vector of the bullet's direction.
If the location of the touch is CGPoint(x : 100, y : 100), the vector cannot be CGVectorMake(100, 100), because it is too big values !
I tested with a moveTo action, the direction of the bullet was perfect, but the bullet stopped on the touch location, and I want that the bullet continue to move forever !
Thanks for your help !