Ok, so Im going straight off Apple's tutorial using a joystick moving an SCNNode for SceneKithere.
I've copied the code and gotten the joystick to both move and rotate the character - but not simultaneously, and not in the right direction relative to the node.
All the correct code is in that download, but what I've done is here is where I get the angle offset of the joystick handle and the float2 from the joystick UI-
characterDirection = float2(Float(padNode.stickPosition.x), -Float(padNode.stickPosition.y))
let direction = theDude.characterDirection(withPointOfView: renderer.pointOfView)
directionAngle = CGFloat(atan2f(direction.x, direction.z))
public func characterDirection(withPointOfView pointOfView: SCNNode?) -> float3 {
let controllerDir = theDude.direction //THIS ISNT BEING UPDATED
if controllerDir.allZero() {
return float3.zero
}
var directionWorld = float3.zero
if let pov = pointOfView {
let p1 = pov.presentation.simdConvertPosition(float3(controllerDir.x, 0.0, controllerDir.y), to: nil)
let p0 = pov.presentation.simdConvertPosition(float3.zero, to: nil)
directionWorld = p1 - p0
directionWorld.y = 0
if simd_any(directionWorld != float3.zero) {
let minControllerSpeedFactor = Float(0.2)
let maxControllerSpeedFactor = Float(1.0)
let speed = simd_length(controllerDir) * (maxControllerSpeedFactor - minControllerSpeedFactor) + minControllerSpeedFactor
directionWorld = speed * simd_normalize(directionWorld)
}
}
return directionWorld
}
I didn't write the last part and still trying to understand it. But what is relevant is I have a float3 and an angle, and they are conflicting when I try to run them both as SCNActions in my renderer update func:
Here is what Apple basically had in update:
// move
if !direction.allZero() {
theDude.characterVelocity = direction * Float(characterSpeed)
var runModifier = Float(1.0)
theDude.walkSpeed = CGFloat(runModifier * simd_length(direction))
// move character - IMPORTANT
theDude.directionAngle = CGFloat(atan2f(direction.x, direction.z))
theDude.node.runAction(SCNAction.move(by: SCNVector3(theDude.characterDirection(withPointOfView: theDude.node)), duration: TimeInterval(40))) //HERE - random time
theDude.isWalking = true
} else {
theDude.isWalking = false
theDude.node.removeAllActions()
}
}
Where on the commented line I applied the move and here Apple had the rotation applied:
var directionAngle: CGFloat = 0.0 {
didSet {
theDude.node.runAction(
SCNAction.rotateTo(x: 0.0, y: directionAngle, z: 0.0, duration: 0.1, usesShortestUnitArc:true))
}
}
They are both happening, problem is I don't know really what to put as my time and my node moves say, left when I have the joystick pointed right, etc because I am not doing the move correctly.
I tried to copy the demo but they have a moving floor, so it is different. What am I doing wrong here?
Related
I try to make a character jump in SceneKit. I currently have a solution, but I am not happy at all with it.
The character movement is based on the updateAtTime renderer function. There are no SCNActions involved. I want it to be moved only by the updateAtTime function.
My scene is entirely based on Apple's Fox demo Game. But In this Demo Game the Fox Character can only fall from heights - not jump. So I made an adaptation to reverse the fall Function into a jump function. The result is kind of woking, but not accurate. Have a look:
As this image shows, the character currently jumps up in a manner that is against natural laws. It accelerates slowly, then gets faster and faster. (Falling down is okay). I'd like to have my character jump in a nice sinus wave, like the right side of my image illustrates. and as it would be in real nature.
Here is some Code, that makes my character currently jump up and down.
// For Jumping (Falling)
if groundAltitude < position.y - threshold || self.isJumping {
if self.isJumpingUp {
accelerationY -= SCNFloat(deltaTime) * gravityAcceleration // approximation of acceleration for a delta time. UP MOVEMENT
} else {
accelerationY += SCNFloat(deltaTime) * gravityAcceleration // approximation of acceleration for a delta time. DOWN MOVEMENT (Orig Apple)
}
} else {
accelerationY = 0
}
// Set Position
position.y -= accelerationY // ORIG
// reset acceleration if we touch the ground
if groundAltitude > position.y {
accelerationY = 0
position.y = groundAltitude
}
As you can see, I am using the variables isJumping and isJumpingUp to control/define the Y-direction in which the character should be moved. So what I do is setting the isJumping Variable to true and also the isJumpingUp, which makes the Character move up. Then at half the time I set the variable isJumpingUp to false, which will reverse the direction, and brings the character down to the ground level.
This all results in a more or less in an inaccurate jump movement as the image visualises.
Then I found this article on SO: How to make my character jump with gravity?
(It has even the ability to run this code snippet in the Browser.)
And this is very close, if not exactly what I am looking for. (But I am only looking for the Y-direction movement stuff)
But each and every attempt to make a Swift/SceneKit adaptation of this results in a total mess. I don't get it managed to implement into my update function the same way as it behaves on the website. And I don't know what I am doing wrong.
Some attempts made the character to jump up very very fast and then never come down. Or the character did not jump at all. I just have no clue... I would like my character to jump up about 1m to 2m in height and then fall down.
Any help in making my character jump accurately is so much appreciated.
For visualisation purposes - here is my entire Movement Function as it currently is:
func moveCharacter(time: TimeInterval) {
// Delta time since last update
if previousUpdateTime == 0.0 { previousUpdateTime = time }
let deltaTime = Float(min(time - previousUpdateTime, 1.0 / 60))
previousUpdateTime = time // GOOD
groundType = GroundType.inTheAir // always make it in the Air, later change, seems to crash the app ??? oder zufall? - RE-ENABLED for testing
// Speed Control
var characterSpeed = deltaTime * self.speedFactor
let characterRunSpeed = deltaTime * self.speedFactor * 3.4
// Remember initial position
let initialPosition = self.node.position
// Move Character Left or Right
if self.isWalkingLeft { self.node.position = self.node.position - SCNVector3(1.0,0.0,0.0) * characterSpeed}
if self.isWalkingRight { self.node.position = self.node.position + SCNVector3(1.0,0.0,0.0) * characterSpeed}
if self.isRunningLeft { self.node.position = self.node.position - SCNVector3(1.0,0.0,0.0) * characterRunSpeed}
if self.isRunningRight { self.node.position = self.node.position + SCNVector3(1.0,0.0,0.0) * characterRunSpeed}
// Character height positioning
var position = self.node.position
var p0 = position
var p1 = position
let maxRise = SCNFloat(1.0) // orig 0.08
let maxJump = SCNFloat(50.0) // orig 20.0
p0.y -= maxJump
p1.y += maxRise
// Do a vertical ray intersection
let results = gameScene.physicsWorld.rayTestWithSegment(from: p1, to: p0, options:[.collisionBitMask: BitMasks.BitmaskCollision, .searchMode: SCNPhysicsWorld.TestSearchMode.closest])
if let result = results.first {
guard (result.node.geometry != nil) else {return}
let groundAltitude = result.worldCoordinates.y
// can the following if statement be made in other way, because of the new guard?
if (result.node.geometry!.firstMaterial) != nil { groundType = groundTypeFromMaterial(material: result.node.geometry!.firstMaterial!) } else { groundType = .rock }
// MARK: Handle Y Position
let threshold = SCNFloat(1e-5)
let gravityAcceleration = SCNFloat(0.18) // 0.18
if groundAltitude < position.y - threshold || self.isJumping {
if self.isJumpingUp {
accelerationY -= SCNFloat(deltaTime) * gravityAcceleration // approximation of acceleration for a delta time. UP
} else {
accelerationY += SCNFloat(deltaTime) * gravityAcceleration // approximation of acceleration for a delta time. DOWN (Orig)
}
}
else {
accelerationY = 0
}
// Set Position
position.y -= accelerationY // orig.
// reset acceleration if we touch the ground
if groundAltitude > position.y {
accelerationY = 0
position.y = groundAltitude
}
// Finally, update the position of the character.
self.node.position = position
// if not touching the ground, we are in the air.
if groundAltitude < position.y - 0.2 {
groundType = .inTheAir
}
} else {
// no result, we are probably out the bounds of the level -> revert the position of the character.
self.node.position = initialPosition
}
}
Well, after some sleep, and reflecting myself, I came on that solution:
(and it works quite nice)
Added this variables to the class:
let gravityDrag : SCNFloat = 0.9999 // kind of slowing down the jump
let jumpPower : SCNFloat = -0.09 // jumps between 1,5m to 2.0m
Then changed the moveCharacter function like so:
// Calc Y Position with acceleration and gravity
if groundAltitude < position.y - threshold || self.isJumping {
accelerationY += SCNFloat(deltaTime) * gravityAcceleration // approximation of acceleration for a delta time. DOWN (Orig)
accelerationY *= gravityDrag
}
else {
accelerationY = 0
}
// Set Position
position.y -= accelerationY // orig.
// reset acceleration if we touch the ground
if groundAltitude > position.y {
isJumping = false
accelerationY = 0
position.y = groundAltitude
}
Trigger a jump like so:
isJumping = true
accelerationY = jumpPower
Hopefully this will help someone, somewhen.
I am working on a game similar to air hockey in SpriteKit for fun and to learn Swift/Xcode. I anticipate the AI to be quite a challenge as there is other elements to the game which will need to be accounted for. I know I'll have to keep tackling each issue one by one. I have created the 2 player mode for the game, and I'm working on AI now. Here is some code I have used for calculating and delegating the impulse from mallet to puck (in the 2 player mode):
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)
{
//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)
//get eleapsed and use to calculate speed6A
if lastTouchTimeStamp != nil
{
let seconds = releventTouch.timestamp - lastTouchTimeStamp!
let velocity = 0.01 * Double(vectorLength) / seconds
//to calculate the vector, the velcity needs to be converted to a CGFloat
let velocityCGFloat = CGFloat(velocity)
//calculate the impulse
let directionVector = CGVector(dx: velocityCGFloat * xOffset / vectorLength, dy: velocityCGFloat * yOffset / vectorLength)
//pass the vector to the scene (so it can apply an impulse to the puck)
delegate?.bottomForce(directionVector, fromBottomPlayer: self)
delegate?.bottomTouchIsActive(bottomTouchIsActive, fromBottomPlayer: self)
}
//update latest touch time for next calculation
lastTouchTimeStamp = releventTouch.timestamp
}
}
I am wondering how I can convert this code for the AI. I have been adding some AI logic to the update function which I believe could also use time stamps and calculate distance traveled between frames to calculate the impulse. I just don't know exactly how to implement that thought. Any help is greatly appreciated :)
Here is some bare bones code I have so far for testing purposes mostly for the AI mode in the update function:
if (ball?.position.y)! < frame.height / 2
{
if (botPlayer?.position.y)! < frame.height * 0.75
{
botPlayer?.position.y += 1
}
}
else
{
if (botPlayer?.position.y)! > (ball?.position.y)!
{
if (botPlayer?.position.y)! - (ball?.position.y)! > frame.height * 0.1
{
botPlayer?.position.y -= 1
}
else
{
botPlayer?.position.y -= 3
}
}
else
{
botPlayer?.position.y += 1
}
}
if ((botPlayer?.position.x)! - (ball?.position.x)!) < 2
{
botPlayer?.position.x = (ball?.position.x)!
}
if (botPlayer?.position.x)! > (ball?.position.x)!
{
botPlayer?.position.x -= 2
}
else if (botPlayer?.position.x)! < (ball?.position.x)!
{
botPlayer?.position.x += 2
}
For AI to make a decision it must have information about the game state. To do this you can write a function that reads all available game data (placement of pucks, player scores, previous moves, etc) and returns the state as a dictionary. Then write a function to take in this dictionary, and output a decision. Consider this workflow in Python.
# we want the AI to make a decision
# start by grabbing game state
gameState = getGameState()
# pass this to the decision function
decision = getDecision( gameState )
# implement the decision
if decision == "move left":
moveLeft()
else:
moveRight()
Is this what you're looking for?
I've started to notice in the game that I'm making that as the fps goes down as more nodes are on the screen, the nodes start to move with a little choppiness to them. I move the nodes with:
let ran = Int(arc4random_uniform(1400));
let monster = SKSpriteNode(texture: text)
monster.position = CGPoint(x: ran, y: 800);
let move = SKAction.moveTo(CGPointMake(monster.position.x, -100), duration: 2);
let remove = SKAction.runBlock { () -> Void in
score += 1;
monster.removeFromParent()
}
monster.runAction(SKAction.sequence([move,remove]));
Can i use delta time to effect the nodes movements? How do i do this? Would it make any difference?
You cannot do much about your FPS slowing down as you add more nodes to your view. Which device is running your app also determines your FPS. Newer models have a much faster CPU.
To use delta time you can do something like this:
-(void)update:(NSTimeInterval) currentTime {
NSTimeInterval delta = currentTime - self.lastUpdateTime;
self.lastUpdateTime = currentTime;
// use the delta time to determine how much your sprites need to move to stay in sync.
}
If you are looking for a Swift version of the code, look at this previous Q&A here.
You cannot slow down or speed up an SKAction in mid run. To adjust speed of movement you will have to move your node manually by either applying a physics force such as CGVectorMake or by changing its x,y positions.
Add this property:
#property (nonatomic) NSTimeInterval lastUpdateTime;
Then in your update method:
-(void)update:(CFTimeInterval)currentTime {
NSTimeInterval delta = currentTime - self.lastUpdateTime;
self.lastUpdateTime = currentTime;
// sanity check
if(delta > 1)
delta = 0;
float distanceToMovePerSecond = 5.0f;
float numberOfFramesPerSecond = 60.0f;
float xPosition = ((distanceToMovePerSecond/numberOfFramesPerSecond) * delta) * 100;
myNode0.position = CGPointMake(myNode0.position.x+xPosition, myNode0.position.y);
}
I have a spritenode and a crosshair node, i want when the player touch the spritenode and move the crosshair also moves.
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
var body = self.nodeAtPoint(location)
if var name: String = body.name {
if body.name == "aim-button" {
crossHair.position = CGPointMake(crossHair.position.x + 10, crossHair.position.y + 10)
}
}
}
}
The crosshair does get move but only in one direction, and i have no idea how to make it accurate depending on the space the touch moved to-from (which is obviously way smaller than what the crosshair should actually move on the screen) and the direction.
for touch: AnyObject in touches {
//Get the current position in scene of the touch.
let location = touch.locationInNode(self)
//Get the previous position in scene of the touch.
let previousLocation = touch.previousLocationInNode(self)
//Calculate the translation.
let translation = CGPointMake(location.x - previousLocation.x, location.y - previousLocation.y)
//Get the current position in scene of the crossHair.
let position = crossHair.position
// Get the bode touched
var body = self.nodeAtPoint(location)
if var name: String = body.name {
if body.name == "aim-button" {
//Set the position of the crosshair to its current position plus the translation.
crossHair.position = CGPointMake(position.x + translation.x * 2, position.y + translation.y * 2)
//Set the position of the body
body.position = location
}
}
}
If the crosshair should move more distance than the touch, just multiply the translation by a factor (i'm using 2 in the example above).
I have an object and 2 GUI texture buttons. I want to rotate the object to the left when I press the left button and to the right while pressing the other one.
Any ideas ?
I have a script that works when I drag my object. I will post the important part:
function Start ()
{
var angles = transform.eulerAngles;
x = angles.y;
// Make the rigid body not change rotation
if (rigidbody)
rigidbody.freezeRotation = true;
}
function LateUpdate ()
{
if (isMouseOverGuiTexture()) return;
if (target && Input.GetMouseButton(0))
{
//0.1 represents the sensitivity of the mouse
x += Input.GetAxis("Mouse X") * xSpeed *0.1; //x rotation
//y -= Input.GetAxis("Mouse Y") * ySpeed *0.1; //y rotation
//y = ClampAngle(y, yMinLimit, yMaxLimit);
var rotation = Quaternion.Euler(y, x, 0);
var position = rotation * Vector3(0.900528, 8.829305, -distance+0.49548)+ target.position;
transform.rotation = rotation;
transform.position = position;
}
}
(Question answered in the comments. See Question with no answers, but issue solved in the comments (or extended in chat) )
The OP wrote:
I solved it by using the following :
var imageLeft: Texture2D; // drag the left button image here
var imageRight: Texture2D; // right button image
var speed: float = 60; // rotate speed in degrees per second
private var rotLeft = false; // object rotates left if true
private var rotRight = false; // object rotates right if true;
function OnGUI()
{
rotLeft = GUI.RepeatButton(Rect(10,10,200,200), imageLeft);
rotRight = GUI.RepeatButton(Rect(230,10,200,200), imageRight);
}
function Update()
{
if (rotLeft) transform.Rotate(0, -speed*Time.deltaTime, 0);
if (rotRight) transform.Rotate(0, speed*Time.deltaTime, 0);
}
I don't know which value Time.deltaTime assumes inside OnGUI - it should be the time since last OnGUI, but I'm not sure. To avoid problems, I placed the real rotation in Update and used two control booleans (rotLeft and rotRight) to communicate with OnGUI.