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.
Related
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'm trying to implement a system where my camera will zoom in on the front face of a gameobject and I can then rotate around it with clamping.
I've managed to get it work, though I'm guessing I've done it totally wrong - Basically I'm expecting actualAngle to be NEGATIVE when I pan to the left and POSITIVE when I pan to the right.
It does work for targets that have no rotation but not for targets that do have a rotation as the centred angle is not zero.
Here's my function:
float _prevMousePosX;
void RotateCamera()
{
//Allow min -30 degrees when panning to the left
minXLimit = -50;
//And max 30 degrees when panning to the right
maxXLimit = 50;
//Work out how much the mouse has moved
var mouseX = Input.GetAxis("Mouse X");
Vector3 angle = target.position + target.rotation * transform.position * -1;
var actualAngle = angle.x;
//The angle is very small so times it by 100
actualAngle = actualAngle * 100;
//This angle is not defaulting to zero before any rotation has occurred when the game object has a rotation other than zero - I think I should be taking into account the existing rotation somehow
Debug.Log(actualAngle);
//The rest of this code is working correctly
if ((actualAngle > minXLimit && actualAngle < maxXLimit && isMoving))
{
//No - Allow rotating in either direction horizontally
if(isMoving) transform.RotateAround(target.transform.position, Vector3.up, mouseX * 1);
} else
{
//Yes, it's more than 30 degrees either left or right
//Work out which way wer're trying to drag the mouse and whether we should allow rotation further in that direction
if (mouseX>0 && actualAngle < minXLimit)
{
//Don't allow?
}
else if (mouseX<0 && actualAngle < minXLimit)
{
//Allow rotating back
if (isMoving) transform.RotateAround(target.transform.position, Vector3.up, mouseX * 1);
}
else
{
//Mouse isn't moving
}
if (mouseX>0 && actualAngle > maxXLimit)
{
//Allow rotating back
if (isMoving) transform.RotateAround(target.transform.position, Vector3.up, mouseX * 1);
}
else if(mouseX<0 && actualAngle > maxXLimit)
{
//Don't allow rotating in this direction any further
} else
{
//Mouse isn't moving
}
}
_prevMousePosX = mouseX;
}
Issue resolved.
//Work out how close we are to facing each other
//We can minus 180 because we'll always be facing each other, but just not always directly
var newAngle = Quaternion.Angle(target.rotation, transform.rotation) -180;
//Then we need to work out whether we are to it's left or it's right
float rotateDirection = (((transform.eulerAngles.y - target.eulerAngles.y) + 360f) % 360f) > 180.0f ? 1 : -1;
var actualAngle = newAngle;
//And finally we can negate our degrees if we're to it's left
actualAngle = actualAngle * rotateDirection;
You can then use actualAngle to clamp your rotation as I do above.
I have a mini project where i have a car that drives on a cylinder and is rotating around the cylinder. When i press left mouse click the car rotates to the left and goes to the left and when i press right mouse click it rotates right and it goes right. What i want is when i release the mouse click buttons i want the car to rotate back to face the forward direction of the cylinder and continue going forward along the cylinder i have a script that is working but it is not rotating the car back to forward direction.
//THIS SCRIPT IS ATTACHED TO THE CAR
void Update(){
//CHECKS IF MOUSE BUTTON ARE CLICKED
if (Input.GetMouseButton (0)) {
rotation = -1;
} else if (Input.GetMouseButton (1)) {
rotation = 1;
} else {
rotation = 0;
}
}
void FixedUpdate(){
//moves the car forward
//rgb is the rigidbody of the car
rgb.MovePosition(rgb.position - transform.right * moveSpeed * Time.fixedDeltaTime);
// THE LINES BELOW ROTATE THE CAR TO THE LEFT OR RIGHT
Vector3 yRot = Vector3.up * rotation * rotation_speed * Time.deltaTime;
Quaternion deltaRot = Quaternion.Euler(yRot);
Quaternion targetRot = rgb.rotation * deltaRot;
rgb.MoveRotation(Quaternion.Slerp(rgb.rotation, targetRot, 50f * Time.fixedDeltaTime));
}
This is because you are setting the variable "rotation" to 0.
Vector3 yRot = Vector3.up * rotation * rotation_speed * Time.deltaTime;
Which results to 0 and your object doesn't rotate.
I would recommend you to do something like this in your update method:
if (Input.GetMouseButton (0))
{
rotation = -1;
}
else if (Input.GetMouseButton (1))
{
rotation = 1;
}
else
{
//Now when the rotation is positive,
//the object starts to rotate towards negative, but stops at 0
//Same is true when the rotation is negative
if (rgb.rotation.y > 0)
{
rotation = -1;
}
else if (rgb.rotation.y < 0)
{
rotation = 1;
}
else
{
//This needs to be added here to stop the object from rotating
//when the y rotation is already 0
rotation = 0;
}
}
Now it rotates towards 0 when you don't press anything. Also don't use Time.deltaTime in FixedUpdate.
Edit:
So I noticed that with this method it doesn't quite never return to 0. It gets very close but not close enough. By adding this to FixedUpdate the y rotation gets rounded to 0.
if (Mathf.Abs(targetRot.y) < 0.00001f)
{
targetRot = Quaternion.Euler(new Vector3(targetRot.x, 0, targetRot.z));
}
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?
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);
}