I'm facing a troublesome problem while trying to create a game engine in threeJS.
It is a math problem, but also a programming problem.
I've implemented a velocity based movement system for the player's avatar - I've used a tank in this example.
Currently, when the player hits a wall, regardless of the angle, the tank invariably stops dead.
However, I want it to be the case that the tank's velocity changes, having been coerced to follow the angle of the wall, and also reduced by a magnitude that is related to that angle.
For example, in FIG A, upon hitting the wall, the Tank continues to try and move forwards, but it's velocity is altered so that it now moves forwards, and sideways, at a reduced rate.
In FIG B, the tank hits the wall dead-on, and its overall velocity reaches 0.
In FIG C, the tank glances off the wall, and its overall velocity is only reduced by a small amount.
I've realised that I need to somehow combine the Tank's velocity vector with the wall's normal vector, to produce the adjusted vector, but I am struggling with how to represent this mathematically / programmatically.
I've tried using: tank.velocity.multiply(wallFaceNormal); (both tank.velocity and wallFaceNormal are Vector3 objects.) but this only seems to work as intended when the wall is either at angles of 0, 90, 180 or 270.
since a tank will not jump or fly, you should be fine with just a 2D-System for your calculation?
i found a link describing the physics of car hitting a solid brick wall.
http://colgatephys111.blogspot.com/2017/12/guardrail-lessens-force-of-impact.html
hope thats gonna help you a bit!
edit:
so, out of curiosity, i asked an theoretical physicist over the phone about your issue.
you got 2 seperate problems to solve:
1. P1 what is the velocity v' while hitting the wall?
2. P2 what is the new angle of the vehicel?
P2 should be fairly easy, considering your tank is adapting the angle of the wall you only need to calculate in which direction the wall is "pointing".
P1 in physics, we would talk about the reduced force and not the velocity, but given a constant limit to the force F1 (eg. your engine) resulting in a constant maxspeed,
and with a given force the wall has on the vehicel F2
v = F1
v' = F1'
F1' = F1 - F2
i think
https://www.thoughtco.com/what-is-the-physics-of-a-car-collision-2698920
explains what to do
Some code provided by a Physicist, which partly worked when I converted it to Javascript and applied it to the program:
Vector3 wallNormal = new Vector3(-0.5, 0.0, 0.5);
Vector3 incomingVelocity = new Vector3(0.0, 0.0, -1.0);
double magnitudeProduct = wallNormal.Length() * incomingVelocity.Length();
double angleBetweenVelocityAndWall = ((-incomingVelocity).Dot(wallNormal)) / (magnitudeProduct);
double newVelocityMagnitude = incomingVelocity.Length() * Math.Sin(angleBetweenVelocityAndWall);
Vector3 upVector =incomingVelocity.Cross(wallNormal);
Vector3 newDirection = wallNormal.Cross(upVector);
Vector3 newVelocity = newDirection.Normalise() * newVelocityMagnitude;
I've done some work on this problem and produced a mini game "framework" that includes an environment collision and movement attenuation utility.
I've written an article that explains how it works, which can be found here. http://www.socket-two.com/main/resource/hdoc-tutorial
But for the sake of the integrity of the thread, here's an adaptation of the portion that describes one of the approaches that can be used to attenuate motion in a ThreeJS simulation:
...
Crucially, my interest has not been to create games that involve large amount of physics, but just to create games where:
A player cannot walk through walls
A player cannot fall through floors
I've made a handful of attempts at implementing a system that would achieve this behaviour, but none of them have really worked satisfactorily. Until now.
In terms of how the ECS fits into the app architecture, it is a utility class. This is its API shape:
class Planeclamp {
constructor({ floors /*Mesh[]*/, walls /*Mesh[]*/ })
getSafePosition(startingPositionIn /*Vector3*/, intendedPositionIn /*Vector3*/) // Returns safePosition, which is a Vector3
}
As you can see, its a class that accepts two arrays of meshes in its constructor: Meshes that should be treated as floors, and meshes that should be treated as walls. Now of course in reality, there is no clear distinction between a steep floor and a shallow-angled wall, but for the purposes of the simulation, the distinction has a very reasonable integrity, and will simplify the environment collision system logic greatly.
Once you've constructed an instance of the Planeclamp class, you can then invoke it's getSafePosition method, to transform a starting position and an intended position into an attenuated position. Being the discerning reader that you are, you will have deduced that the attenuated position is the intended position, having been changed a bit if any collisions have been detected by the utility.
This is how it can be used in the game loop, to ensure a player does not pass through walls or floors:
const planeclamp = new Planeclamp({
floors: [someFloorMesh, someOtherMesh],
walls: [houseMesh, perimeterMesh, truckMesh],
});
const player = new Player();
console.log(player.cage); // Object3D
let playerPreviousPosition = player.cage.position; // Vector3
function gameLoop(delta) {
const playerIntendedPosition = new Three.Vector3(
playerPreviousPosition.x,
playerPreviousPosition.y + (10 * delta), // i.e. Gravity
playerPreviousPosition.z + (1 * delta), // i.e. Walking forwards
);
let {
safePosition, // Vector3
grounded, // Boolean
groundMaterial, // String
} = planeclamp.getSafePosition(playerPreviousPosition, playerIntendedPosition);
player.cage.position.copy(safePosition);
playerPreviousPosition = player.cage.position; // Vector3
}
And thats about it! If you would like to use this utility, you can find it in the repository. But if you would like to know more about the logic behind its workings, read on.
The Planeclamp.getSafePosition method works out a safe position in two stages. Firstly, it uses a vertical raycaster to take a look at what is underneath the player, to then see if it should stop the player from moving downwards any further. Secondly, it uses horizontal raycasters to see if it should stop the player from moving horizontally. Lets look at the vertical constraint procedure first - this is the more simple of the two steps.
// Before we do anything, create a variable called "gated".
// This will contain the safe new position that we will return at the end of
// the function. When creating it, we let it default to the
// intended position. If collisions are detected throughout the lifecycle
// of this function, these values will be overwritten.
let gated = {
x: intendedPosition.x,
y: intendedPosition.y,
z: intendedPosition.z,
};
// Define the point in 3D space where we will shoot a ray from.
// For those who haven't used raycasters before, a ray is just a line with a direction.
// We use the player's intended position as the origin of the ray, but we
// augment this by moving the origin up a little bit (backStepVert) to prevent tunneling.
const start = intendedPosition.clone().sub(new Three.Vector3(
0,
(backStepVert * -1) - (heightOffset / 2),
0)
);
// Now, define the direction of the ray, in the form of a vector.
// By giving the vector X and Z values of 0, and a Y value of -1,
// the ray shoots directly downwards.
const direction = new Three.Vector3(0, -1, 0).normalize();
// We now set the origin and direction of a raycaster that we instantiated
// in the class constructor method.
this.raycasters.vert.set(start, direction);
// Now, we use the `intersectObjects` method of the ray.
// This will return to us an array, filled with information about each
// thing that the ray collided with.
const dirCollisions = this.raycasters.vert.intersectObjects(this.floors, false);
// Initialise a distanceToGround, a grounded variable, and a groundMaterial variable.
let distanceToGround = null;
let grounded = false;
let groundMaterial = null;
// If the dirCollisions array has at least one item in it, the
// ray passed through one of our floor meshes.
if (dirCollisions.length) {
// ThreeJS returns the nearest intersection first in the collision
// results array. As we are only interested in the nearest collision,
// we pluck it out, and ignore the rest.
const collision = dirCollisions[0];
// Now, we work out the distance between where the players feet
// would be if the players intended position became the players
// actual position, and the collided object.
distanceToGround = collision.distance - backStepVert - heightOffset;
// If the distance is less than 0, then the player will pass through
// the groud if their intended position is allowed to become
// their actual position.
if (distanceToGround < 0) {
// We dont want that to hapen, so lets set the safe gated.y coordinate
// to the y coordinate of the point in space at which the collision
// happened. In other words, exactly where the ground is.
gated.y = intendedPosition.y - distanceToGround;
// Make a note that the player is now grounded.
// We return this at the end of the function, along with
// the safe position.
grounded = true;
// If the collided object also has a groundMaterial set inside
// its userData (the place that threeJS lets us attach arbitrary
// info to our objects), also set the groundMaterial. This is
// also returned at the end of the function alongside the grounded
// variable.
if (collision.object.userData.groundMaterial) {
groundMaterial = collision.object.userData.groundMaterial;
}
}
}
And thats it for vertical environment constraints. Simples!
The horizontal environment constraint system is a bit more complex. But in its essence, what it does is:
Work out the horizontal direction the player is travelling in. In olde worlde terms, this can be thought of as North, South, SouthEast, SouthSouthWest etc, but in ThreeJS it is represented by a Vector.
Cast a ray in the direction that the player is travelling in.
Use the ray to find out if allowing the players intended position would cause the player to pass through any of the wall meshes.
And it is at this point that the horizontal ECS becomes more complex than the vertical ECS. With the vertical ECS, if a collision happens, we can just set the players Y position to the Y position of the point at which the collision happened - effectively halting the players Y movement. However, it we did this for horizontal movement, it would make for a very frustrating game experience.
If the player was running head on into a wall, and was stopped dead in their tracks, this would be fine. But if the player moved into the wall at a very shallow angle, and merely grazed it, it would appear that they had "gotten stuck" on the wall, and would find themselves having to reverse away from it, and take care not to touch it again.
What we actually want to happen, is have the player's horizontal velocity attenuated, so that they move along the wall. Therefore, the horizontal ECS proceeds as follows:
Obtain the normal of the surface that was collided with. (For our purposes, a normal can be described as the direction that the wall is facing)
Inspect the difference between the wall normal direction, and the player's movement direction.
Use the difference to work out a safe position, which is the point in space that the collision happened, incremented by a vector that is horizontally perpendicular to the wall normal, multiplied by the cross product of the players input direction and the wall normal.
...
Here is the final utility class, in full:
import * as Three from '../../../vendor/three/three.module.js';
class Planeclamp {
constructor({
scene,
floors = [],
walls = [],
drawRays = true,
} = {}) {
this.drawRays = drawRays;
this.floors = [];
this.walls = [];
this.scene = scene;
this.objects = [];
// Init collidable mesh lists
this.addFloors(floors);
this.addWalls(walls);
// Create rays
this.raycasters = {
vert: new Three.Raycaster(),
horzLeft: new Three.Raycaster(),
horzRight: new Three.Raycaster(),
correction: new Three.Raycaster(),
};
}
setDrawRays(draw) {
this.drawRays = draw;
}
addFloor(floor) {
this.floors.push(floor);
}
removeFloor(floor) {
this.floors = this.floors.filter(thisFloor => thisFloor !== floor);
}
addFloors(floors) {
floors.forEach(floor => this.addFloor(floor));
}
resetFloors() {
this.floors = [];
}
addWall(wall) {
this.walls.push(wall);
}
removeWall(wall) {
this.walls = this.walls.filter(thisWall => thisWall !== wall);
}
addWalls(walls) {
walls.forEach(wall => this.addWall(wall));
}
resetWalls() {
this.walls = [];
}
getSafePosition(startingPositionIn, intendedPositionIn, {
collisionPadding = .5,
heightOffset = 0,
} = {}) {
// ------------------ Setup -------------------
// Parse args
const startingPosition = startingPositionIn.clone();
const intendedPosition = intendedPositionIn.clone();
let grounded = false;
let groundMaterial = null;
// Augmenters
const backStepVert = 50;
const backStepHorz = 5;
const backStepCorrection = 5;
// Prepare output
let gated = {
x: intendedPosition.x,
y: intendedPosition.y,
z: intendedPosition.z,
};
// Clean up previous debug visuals
this.objects.map(object => this.scene.remove(object));
this.objects = [];
// ------------------ Vertical position gating -------------------
// Adjust vertical position in gated.y.
// Store grounded status in grounded.
const start = intendedPosition.clone().sub(new Three.Vector3(
0,
(backStepVert * -1) - (heightOffset / 2),
0)
);
const direction = new Three.Vector3(0, -1, 0).normalize();
this.raycasters.vert.set(start, direction);
const dirCollisions = this.raycasters.vert.intersectObjects(this.floors, false);
if (this.drawRays) {
const arrowColour = dirCollisions.length ? 0xff0000 : 0x0000ff;
const arrow = new Three.ArrowHelper(this.raycasters.vert.ray.direction, this.raycasters.vert.ray.origin, 300, arrowColour);
this.objects.push(arrow);
}
let distanceToGround = null;
if (dirCollisions.length) {
const collision = dirCollisions[0];
distanceToGround = collision.distance - backStepVert - heightOffset;
if (distanceToGround < 0) {
gated.y = intendedPosition.y - distanceToGround;
grounded = true;
if (collision.object.userData.groundMaterial) {
groundMaterial = collision.object.userData.groundMaterial;
}
}
}
// ------------------ Horizontal position gating -------------------
const horizontalOutputPosition = (() => {
// Init output position
const outputPosition = new Three.Vector3(intendedPosition.x, 0, intendedPosition.z);
// Store normalised input vector
const startingPos = startingPosition.clone();
const intendedPos = intendedPosition.clone();
startingPos.y = startingPositionIn.y + .5;
intendedPos.y = startingPositionIn.y + .5;
let inputVector = intendedPos.clone().sub(startingPos).normalize();
// Work out distances
const startingIntendedDist = startingPos.distanceTo(intendedPos);
const inputSpeed = startingIntendedDist;
// Define function for moving ray left and right
function adj(position, offset) {
const rayAdjuster = inputVector
.clone()
.applyAxisAngle(new Three.Vector3(0, 1, 0), Math.PI / 2)
.multiplyScalar(.5)
.multiplyScalar(offset);
return position.clone().add(rayAdjuster);
}
// Work out intersections and collision
let collisions = {
left: {
collision: null
},
right: {
collision: null
}
};
Object.keys(collisions).forEach(side => {
const rayOffset = side === 'left' ? -1 : 1;
const rayStart = adj(startingPos.clone().sub(inputVector.clone().multiplyScalar(2)), rayOffset);
const startingPosSide = adj(startingPos, rayOffset);
const intendedPosSide = adj(intendedPos, rayOffset);
const startingIntendedDistSide = startingPosSide.distanceTo(intendedPosSide);
const rayKey = 'horz' + _.startCase(side);
this.raycasters[rayKey].set(rayStart, inputVector);
const intersections = this.raycasters[rayKey].intersectObjects(this.walls, true);
for (let i = 0; i < intersections.length; i++) {
if (collisions[side].collision) break;
const thisIntersection = intersections[i];
const startingCollisionDist = startingPosSide.distanceTo(thisIntersection.point);
if (startingCollisionDist - collisionPadding <= startingIntendedDistSide) {
collisions[side].collision = thisIntersection;
collisions[side].offset = rayOffset;
}
}
if (inputSpeed && this.drawRays) {
this.objects.push(new Three.ArrowHelper(this.raycasters[rayKey].ray.direction, this.raycasters[rayKey].ray.origin, 300, 0x0000ff));
}
});
const [ leftCollision, rightCollision ] = [ collisions.left.collision, collisions.right.collision ];
const collisionData = (leftCollision?.distance || Infinity) < (rightCollision?.distance || Infinity) ? collisions.left : collisions.right;
if (collisionData.collision) {
// Var shorthands
const collision = collisionData.collision;
const normalVector = collision.face.normal.clone();
normalVector.transformDirection(collision.object.matrixWorld);
normalVector.normalize();
// Give output a baseline position that is the same as the collision position
let paddedCollision = collision.point.clone().sub(inputVector.clone().multiplyScalar(collisionPadding));
paddedCollision = adj(paddedCollision, collisionData.offset * -1);
outputPosition.x = paddedCollision.x;
outputPosition.z = paddedCollision.z;
if (leftCollision && rightCollision && leftCollision.face !== rightCollision.face) {
return startingPos;
}
// Work out difference between input vector and output / normal vector
const iCAngleCross = inputVector.clone().cross(normalVector).y; // -1 to 1
// Work out output vector
const outputVector = (() => {
const ivn = inputVector.clone().add(normalVector);
const xMultiplier = ivn.x > 0 ? 1 : -1;
const zMultiplier = ivn.z > 0 ? 1 : -1;
return new Three.Vector3(
Math.abs(normalVector.z) * xMultiplier,
0,
Math.abs(normalVector.x) * zMultiplier,
).normalize();
})();
if (inputSpeed && this.drawRays) {
this.objects.push(new Three.ArrowHelper(normalVector, startingPos, 300, 0xff0000));
}
// Work out output speed
const outputSpeed = inputSpeed * Math.abs(iCAngleCross) * 0.8;
// Increment output position with output vector X output speed
outputPosition.add(outputVector.clone().multiplyScalar(outputSpeed));
}
// ------------------ Done -------------------
return outputPosition;
})();
gated.x = horizontalOutputPosition.x;
gated.z = horizontalOutputPosition.z;
// ------------------ Culmination -------------------
// Add debug visuals
this.objects.map(object => this.scene.add(object));
// Return gated position
const safePosition = new Three.Vector3(gated.x, gated.y, gated.z);
return { safePosition, grounded, groundMaterial };
}
}
export default Planeclamp;
I have a player and a few NPCs.
The NPCs have random movement, and I control my players movement. They both have RigidBody2D to deal with physics and BoxCollider2D to deal with Collisions.
However, when I walk into a NPC my player pushes it. Same thing if a NPC moves into my player while the player stands still.
I can't set the mass of either object to some extreme number since that will interfere with how they behave with other objects in my game.
What I want:
When an NPC collides with the player, the NPC stops (I get this effect if I set player mass to ex. 1000, but then the player can push the NPC, which I dont want), and the NPC acts as a "wall", i.e it doesnt move, but nor can the player push it around. How can I do this?
EDIT: So I created my own method for it:
void OnCollisionEnter2D(Collision2D other){
if (other.gameObject.name == "Player") {
collidedWithPlayer = true; //we only move if !collidedWithPlayer
isMoving = false; //stop moving
myRigidBody.mass = 1000; //turn NPC into "wall"
}
}
void OnCollisionExit2D(Collision2D other){
if (other.gameObject.name == "Player") {
collidedWithPlayer = false;
waitCounter = waitTime; //stop NPC from starting to move right after we exit
myRigidBody.mass = 1;
}
}
I mean this works, but is there no native method to do this?
What you are trying to do is essentially use a "realistic" physics engine to create rather unrealistic physics. That's why it's not supported by Unity's built-in functions. Furthermore, you are correct in assuming that messing with the object masses is not a good idea.
Here's one suggestion that avoids playing with mass. It's a bit kludgey, but give it a try and see if it works for you. (I assume your player rigidbody is not Kinematic?)
Step 1: Create 2 new layers; call them NPCWall and PlayerWall. Setup 2D physics so that player collides with NPCWall and NPC collides with PlayerWall, but player does not collide with NPCs. (If your NPCs and player are on the same layer, then of course put them on 2 separate layers.)
Step 2: Create an NPCWall prefab that uses the same kind of collider as the NPCs. I assume you only have one size of NPC. Likewise, create a PlayerWall prefab that uses the same kind of collider as the player. Set the NPCWall prefab to NPCWall layer, and PlayerWall prefab to PlayerWall layer.
Step 3: We can't parent the NPCWall to the NPC, because it would end up as part of the rigidbody. Therefore add a simple script to the NPCWall and PlayerWall:
public class TrackingWall
{
//This invisible wall follows an NPC around to block the player.
//It also follows the player around to block NPCs.
Transform followTransform;
public void Init(Transform targetTrans)
{
followTransform = targetTrans;
transform.position = followTransform.position;
transform.rotation = followTransform.rotation;
}
void Update()
{
if (followTransform == null)
Destroy(gameObject);
transform.position = followTransform.position;
transform.rotation = followTransform.rotation;
}
}
Step 4: In the NPC and player scripts:
TrackingWall myWallPrefab;
void Start()
{
[whatever else you are doing in Start()]
TrackingWall myWall = Instantiate<TrackingWall>(myWallPrefab);
myWall.Init(transform);
}
Obviously, for NPCs, myWallPrefab should be set to the NPCWall prefab, and for players, myWallPrefab should be set to the PlayerWall prefab.
In theory this should give each character an impenetrable, immovable wall that only moves when they do, prevents other characters from pushing them, and cleans itself up when they are destroyed. I can't guarantee it will work though!
I have a drone following a path for movement. That is, it doesn't use a rigidbody so I don't have access to velocity or magnitude and such. It follows the path just fine, but I would like to add banking to it when it turns left or right. I use a dummy object in front of the drone, thinking I could calculate the bank/tilt amount using the transform vectors from the two objects.
I've been working on this for days as I don't have a lot of math skills. Basically I've been copying pieces of code trying to get things to work. Nothing I do works to make the drone bank. The following code manages to spin (not bank).
// Update is called once per frame
void Update () {
Quaternion rotation = Quaternion.identity;
Vector3 dir = (dummyObject.transform.position - this.transform.position).normalized;
float angle = Vector3.Angle( dir, transform.up );
float rollAngle = CalculateRollAngle(angle);
rotation.SetLookRotation(dir, transform.right);// + rollIntensity * smoothRoll * right);
rotation *= Quaternion.Euler(new Vector3(0, 0, rollAngle));
transform.rotation = rotation;
}
/// <summary>
/// Calculates Roll and smoothes it (to compensates for non C2 continuous control points algorithm) /// </summary>
/// <returns>The roll angle.</returns>
/// <param name="rollFactor">Roll factor.</param>
float CalculateRollAngle(float rollFactor)
{
smoothRoll = Mathf.Lerp(smoothRoll, rollFactor, rollSmoothing * Time.deltaTime);
float angle = Mathf.Atan2(1, smoothRoll * rollIntensity);
angle *= Mathf.Rad2Deg;
angle -= 90;
TurnRollAngle = angle;
angle += RollOffset;
return angle;
}
Assuming you have waypoints the drone is following, you should figure out the angle between the last two (i.e. your "now-facing" and "will be facing" directions). The easy way is to use Vector2.Angle.
I would use this angle to determine the amount I'll tilt the drone's body: the sharper the turn, the harder the banking. I would use a ratio value (public initially so I can manipulate it from the editor).
Next, instead of doing any math I would rely on the engine to do the rotation for me - so I would go for Transform.Rotate function.In case banking can go too high and look silly, I would set a maximum for that and Clamp my calculated banking angle between zero and max.
Without knowing exactly what you do and how, it's not easy to give perfect code, but for a better understand of the above, here's some (untested, i.e. pseudo) code for the solution I visualize:
public float turnSpeed = 7.0f; //the drone will "rotate toward the new waypoint" by this speed
//bankSpeed+turnBankRatio must be two times "faster" (and/or smaller degree) than turning, see details in 'EDIT' as of why:
public float bankSpeed = 14.0f; //banking speed
public float turnBankRatio = .5f; //90 degree turn == 45 degree banking
private float turnAngle = 0.0f; //this is the 'x' degree turning angle we'll "Lerp"
private float turnAngleABS = 0.0f; //same as turnAngle but it's an absolute value. Storing to avoid Mathf.Abs() in Update()!
private float bankAngle = 0.0f; //banking degree
private bool isTurning = false; //are we turning right now?
//when the action is fired for the drone it should go for the next waypoint, call this guy
private void TurningTrigger() {
//remove this line after testing, it's some extra safety
if (isTurning) { Debug.LogError("oups! must not be possible!"); return; }
Vector2 droneOLD2DAngle = GetGO2DPos(transform.position);
//do the code you do for the turning/rotation of drone here!
//or use the next waypoint's .position as the new angle if you are OK
//with the snippet doing the turning for you along with banking. then:
Vector2 droneNEW2DAngle = GetGO2DPos(transform.position);
turnAngle = Vector2.Angle(droneOLD2DAngle, droneNEW2DAngle); //turn degree
turnAngleABS = Mathf.Abs(turnAngle); //avoiding Mathf.Abs() in Update()
bankAngle = turnAngle * turnBankRatio; //bank angle
//you can remove this after testing. This is to make sure banking can
//do a full run before the drone hits the next waypoint!
if ((turnAngle * turnSpeed) < (bankAngle * bankSpeed)) {
Debug.LogError("Banking degree too high, or banking speed too low to complete maneuver!");
}
//you can clamp or set turnAngle based on a min/max here
isTurning = true; //all values were set, turning and banking can start!
}
//get 2D position of a GO (simplified)
private Vector2 GetGO2DPos(Vector3 worldPos) {
return new Vector2(worldPos.x, worldPos.z);
}
private void Update() {
if (isTurning) {
//assuming the drone is banking to the "side" and "side" only
transform.Rotate(0, 0, bankAngle * time.deltaTime * bankSpeed, Space.Self); //banking
//if the drone is facing the next waypoint already, set
//isTurning to false
} else if (turnAngleABS > 0.0f) {
//reset back to original position (with same speed as above)
//at least "normal speed" is a must, otherwise drone might hit the
//next waypoint before the banking reset can finish!
float bankAngle_delta = bankAngle * time.deltaTime * bankSpeed;
transform.Rotate(0, 0, -1 * bankAngle_delta, Space.Self);
turnAngleABS -= (bankAngle_delta > 0.0f) ? bankAngle_delta : -1 * bankAngle_delta;
}
//the banking was probably not set back to exactly 0, as time.deltaTime
//is not a fixed value. if this happened and looks ugly, reset
//drone's "z" to Quaternion.identity.z. if it also looks ugly,
//you need to test if you don't """over bank""" in the above code
//by comparing bankAngle_delta + 'calculated banking angle' against
//the identity.z value, and reset bankAngle_delta if it's too high/low.
//when you are done, your turning animation is over, so:
}
Again, this code might not perfectly fit your needs (or compile :P), so focus on the idea and the approach, not the code itself. Sorry for not being able right now to put something together and test myself - but I hope I helped. Cheers!
EDIT: Instead of a wall of text I tried to answer your question in code (still not perfect, but goal is not doing the job, but to help with some snippets and ideas :)
So. Basically, what you have is a distance and "angle" between two waypoints. This distance and your drone's flight/walk/whatever speed (which I don't know) is the maximum amount of time available for:
1. Turning, so the drone will face in the new direction
2. Banking to the side, and back to zero/"normal"
As there's two times more action on banking side, it either has to be done faster (bankSpeed), or in a smaller angle (turnBankRatio), or both, depending on what looks nice and feels real, what your preference is, etc. So it's 100% subjective. It's also your call if the drone turns+banks quickly and approaches toward the next waypoint, or does things in slow pace and turns just a little if has a lot of time/distance and does things fast only if it has to.
As of isTurning:
You set it to true when the drone reached a waypoint and heads out to the next one AND the variables to (turn and) bank were set properly. When you set it to false? It's up to you, but the goal is to do so when the maneuver is finished (this was buggy in the snippet the first time as this "optimal status" was not possible to ever be reached) so he drone can "reset banking".For further details on what's going on, see code comments.Again, this is just a snippet to support you with a possible solution for your problem. Give it some time and understand what's going on. It really is easy, you just need some time to cope ;)Hope this helps! Enjoy and cheers! :)
I'm trying to implement simple continuous collision detection for my pong game however i'm not sure i'm implementing or understand this right. AFAIR continuous collision detection is used for fast moving objects that may pass through another object circumventing normal collision detection.
So what I tried was that because the only fast moving object I have is a ball I would just need the position of the ball, its move speed, and the position of the object we are comparing to.
From this I figured it would be best that for example if the ball's move speed indicated it was moving left, I would compare it's left-most bound to the right-most bound of the other object. From this I would step through by adding the move speed to the left-most bound of the ball and compare to make sure it's greater than the other objects right bound. This would show that there is no left right collision.
I have something somewhat working, but unfortunately, the ball starts bouncing normally for a while then it acts as if it hits a paddle when nothing is there.
I'm a bit lost, any help would be appreciated!
static bool CheckContinuousCollision(PActor ball, PRect ballRect, PActor other, PRect otherRect)
{
PVector ballMoveSpeed;
int ballXLimit;
int ballYLimit;
ballMoveSpeed = ball.moveSpeed;
// We are moving left
if ( sgn(ball.moveSpeed.x) < 0 )
{
ballXLimit = std.math.abs(ballMoveSpeed.x) / 2;
for ( int i = 0; i <= ballXLimit; i++ )
{
if ( ballRect.Left < otherRect.Right && otherRect.Left < ballRect.Left)
{
return true;
}
ballRect.Left -= i;
}
}
//We are moving right
if ( sgn(ball.moveSpeed.x) > 0)
{
ballXLimit = std.math.abs(ballMoveSpeed.x) / 2;
for ( int i = 0; i < ballXLimit; i ++ )
{
if ( ballRect.Right > otherRect.Left && ballRect.Right < otherRect.Right )
{
return true;
}
ballRect.Right += i;
}
}
// we are not moving
if ( sgn(ball.moveSpeed.x) == 0)
{
return false;
}
}
You seem to be checking the collision of only one dimension, i.e the X dimension of your ball versus your Other.
What you probably want is to compare whether the two objects collide in 2d space. This can be easily done by adjusting each objects Bounding Rectangle and checking whether the rectangles overlap. Then in your for loop you can adjust your Ball rectangle accordingly
I created this function to move a unit along way points that are saved in list_. Every Unit has its own list_. move() is initially called with the speed (distance/step) every step. Then depending on the distance to the next way point three possible actions are taken.
Can you suggest any improvements?
void Unit::move(qreal maxDistance)
{
// Construct a line that goes from current position to next waypoint
QLineF line = QLineF(pos(), list_.firstElement().toPointF());
// Calculate the part of this line that can be "walked" during this step.
qreal part = maxDistance / line.length();
// This step's distance is exactly the distance to next waypoint.
if (part == 1) {
moveBy(line.dx(), line.dy());
path_.removeFirst();
}
// This step's distance is bigger than the distance to the next waypoint.
// So we can continue from next waypoint in this step.
else if (part > 1)
{
moveBy(line.dx() , line.dy());
path_.removeFirst();
if (!path_.isEmpty())
{
move(maxDistance - line.length());
}
}
// This step's distance is not enough to reach next waypoint.
// Walk the appropriate part of the length.
else /* part < 1 */
{
moveBy(line.dx() * part, line.dy() * part);
}
}
I'll hate myself for suggesting a deprecated way of doing things, but there's no reference to the replacing method :(
QGraphicsItemAnimation
It has addStep and linear interpolation stuff as a convenience.
It seems Qt devs would like you to use QTimeLine itself as a replacement.
I'd use Qt Animation Framework, more precisely QPropertyAnimation:
// I use QPainterPath to calculate the % of whole route at each waypoint.
QVector<qreal> lengths;
QPainterPath path;
path.moveTo(list_.first());
lengths.append(0);
foreach (const QPointF &waypoint, list_.mid(1)) {
path.lineTo(waypoint);
lengths.append(path.length());
}
// KeyValues is typedef for QVector< QPair<qreal, QVariant> >
KeyValues animationKeyValues;
for (int i(0); i != lenghts.count(); ++i) {
animationKeyValues.append(qMakePair(path.percentAtLength(lenghts.at(i)), list_.at(i)));
}
// I assume unit is a pointer to a QObject deriving Unit instance and that
// Unit has QPointF "position" property
QPropertyAnimation unitAnimation(unit, "position");
unitAnimation.setKeyValues(animationKeyValues);
unitAnimation.setDuration(/* enter desired number here */);
unitAnimation.start();
I haven't tested this solution, but you should get the general idea.