How to limit player's velocity only when it is accelerating? - unity3d

I have a 2.5D space shooter game in progress that uses Unity's built-in physics. Everything happens in 2D space but all the models are 3D.
The player (a space ship) can rotate using a controller axis and can accelerate when a button is held down (e.g. xbox controller's A button).
There is a limit on how fast the player can move (maxEngineSpeed) and I clamp the magnitude of the RigidBody's velocity in FixedUpdate as follows:
if (rb.velocity.magnitude > maxEngineSpeed)
{
rb.velocity = Vector2.ClampMagnitude(rb.velocity, maxEngineSpeed);
}
Now the problem is that this prevents the veclocity from ever reaching a value higher than maxEngineSpeed .
I want a behaviour that only limits the velocity when the player is accelerating. If the player somehow gains more speed from a coillision or from a bullet hit, then the velocity should not be limited. We can think it like the space ship not having enough power in its engines to go any faster. It's like linear drag but only when accelerating (when not accelerating, the ship doesn't decelerate at all). I have power-ups that grant the player more maximum speed, so it's important.
How would this be implemented? I've tried to only limit the velocity when the player is accelerating, but then it clamps it immediately to specified value and it looks unnatural. Would a couroutine work that would slowly reduce the magnitude when accelerating? But then it would have to take account the direction of the player and current velocity?
EDIT: Clarification: in practise what I would like to have is to ask a RigidBody "if I apply this force to you while you're moving faster than maxEngineSpeed, would it increase your speed? If it would, don't apply the force, if it would decrease your speed, then apply it".
EDIT: changed the maxSpeed variable name to maxEngineSpeed for more clarity.

Remove the clamping in FixedUpdate. Instead, add a check where you add Velocity (where you detect Xbox Controllers 'A' is pressed).
Something like:
if(Input.GetButton("Xbox-A"))
{
if(rb.velocity.magnitude < scaledMaxSpeed)
{
rb.addForce(...);
}
}
So if you are faster than your max-speed, the ship cannot accelerate more (by own power).

Pull down & Drag
There are so many ways to achieve what you want. Below I show two possible methods with a working demo to allow you to get a bit of a feel for how the perform and differ. Also link at bottom to another demo.
Pull down
You can pull down the velocity by defining a max over speed and a over speed drag coefficient
The pull down method
Define settings
float pullDown = 0.1f; // dimensionless > 0, < 1
float maxOverSpeed = 5.0f;
float maxSpeed = 4.0f
float acceleration = 0.1f;
Per frame
if (accelerate && speed < maxSpeed) { speed += acceleration }
// clamp max over speed
speed = speed > maxOverSpeed ? maxOverSpeed : speed;
float speedAdjust = speed - maxSpeed;
// pull speed down if needed
speed -= speedAdjust > 0.0f ? speedAdjust * pullDown : 0.0f;
// set the velocity magnitude to the new speed
Personally I don't like this method as it is a coasting model, ship gets to speed an holds it, there is no deceleration, but it does give finer control over velocity.
Drag
My preferred method is to use a simple drag coefficient. Slight modification to add extra draw when over speed
However this makes is difficult to know what the max speed will be given some acceleration. There is a formula that will give you a drag coefficient to match a max speed for acceleration, or acceleration to match a max speed for a drag coefficient, but off the top of my head I can not remember it as its been years since I found I needed to use it.
I wing it and define an approximation, test it, and refine till I get what feels right. In reality if ask what is the max speed of the player? All i know is not too fast and not too slow. :P
The drag method
Define
float acceleration = 0.1f;
float drag = 1.0f - 0.021f;
float overSpeedDrag = 1.0f - 0.026f;
float maxSpeed = 4;
Per frame
// apply drag depending on speed
speed *= speed > maxSpeed ? overSpeedDrag : drag;
if (accelerate) { speed += acceleration }
// set the velocity magnitude to the new current speed
Example
These methods as code do not give much of a feel for the actual results so the following snippet implements both methods so you can see and feel how they work.
The code is at the top (in JavaScript) the two different methods are flagged PULL_DOWN, DRAG in the function update() {
Ship speeds are in pixels per second (Pps)
Both ships have same acceleration constant, however ship B (drag method) does not accelerate at a constant rate.
Ship A will coast, ship B will always come to a stop.
Click bump to kick the ship's velocity.
const accelFunction = {
get vel() { return new Vec2(0, 0) },
speed: 0,
acceleration: 0.1,
maxSpeed: 4,
// drag constants
drag: 1 - 0.0241,
overSpeedDrag: 1 - 0.0291,
// pulldown constants;
overSpeed: 5,
pullDown: 0.1,
update() {
if (this.method === DRAG) { // Drag method
this.speed *= this.speed > this.maxSpeed ? this.overSpeedDrag: this.drag;
if (this.accelerate) { this.speed += this.acceleration }
} else { // Pull down method
if (this.accelerate && this.speed < this.maxSpeed) { this.speed += this.acceleration }
this.speed = this.speed > this.maxOverSpeed ? this.maxOverSpeed : this.speed;
var speedAdjust = this.speed - this.maxSpeed;
this.speed -= speedAdjust > 0 ? speedAdjust * this.pullDown : 0;
}
// move ship
this.vel.length = this.speed;
this.pos.add(this.vel);
},
}
/* rest of code unrelated to anwser */
requestAnimationFrame(start);
const ctx = canvas.getContext("2d");
const PULL_DOWN = 0;
const DRAG = 1;
var shipA, shipB;
var bgPos;
function Ship(method, control, controlBump) { // creates a Player ship
control.addEventListener("mousedown",() => API.go());
control.addEventListener("mouseup",() => API.coast());
control.addEventListener("mouseout",() => API.coast());
controlBump.addEventListener("click",() => API.bump());
const API = {
...accelFunction,
pos: new Vec2(100, 50 + method * 50),
method, // 0 drag method, 1 pulldown
draw() {
ctx.setTransform(1,0,0,1,this.pos.x - bgPos.x, this.pos.y)
ctx.strokeStyle = "#FFF";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.lineTo(20, 0);
ctx.lineTo(-20, -20);
ctx.lineTo(-20, 20);
ctx.closePath();
ctx.stroke();
ctx.fillText(this.method ? "B" : "A", -11, 3);
ctx.fillText((this.speed * 60 | 0) + "Pps", 80, 3);
if (this.accelerate) {
ctx.strokeStyle = "#FF0";
ctx.beginPath();
ctx.lineTo(-20, -10);
ctx.lineTo(-30 - Math.rand(0,10), 0);
ctx.lineTo(-20, 10);
ctx.stroke();
}
},
focus: false,
reset() {
this.focus = false;
this.vel.zero();
this.pos.init(100, 50 + this.method * 50);
this.speed = 0;
this.accelerate = false;
},
go() {
this.accelerate = true;
this.focus = true;
if (this.method === 1) { shipA.reset() }
else { shipB.reset() }
},
coast() {
this.accelerate = false;
},
bump() {
this.speed += 1;
},
};
return API;
}
function start() {
init();
requestAnimationFrame(mainLoop);
}
function mainLoop() {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,500,170);
shipA.update();
shipB.update();
bgPos.x = shipA.focus ? shipA.pos.x - 50 : shipB.pos.x - 50 ;
drawBG(bgPos);
shipA.draw();
shipB.draw();
requestAnimationFrame(mainLoop);
}
function drawBG(bgPos) {
ctx.fillStyle = "#FFF";
ctx.beginPath();
const bgX = -bgPos.x + 100000;
for (const p of background) {
x = (p.x + bgX) % 504 - 2;
ctx.rect(x, p.y, 2, 2);
}
ctx.fill();
}
const BG_COUNT = 200;
const background = [];
function init() {
ctx.font = "16px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
bgPos = new Vec2();
shipA = Ship(PULL_DOWN, goA, bumpA);
shipB = Ship(DRAG, goB, bumpB);
var i = BG_COUNT;
while (i--) {
background.push(new Vec2(Math.rand(0, 10000), Math.rand(-1, 170)));
}
}
/* Math LIB Vec2 and math extensions */
Math.rand = (m, M) => Math.random() * (M - m) + m;
function Vec2(x = 0, y = (temp = x, x === 0 ? (x = 0 , 0) : (x = x.x, temp.y))) { this.x = x; this.y = y }
Vec2.prototype = {
init(x, y = (temp = x, x = x.x, temp.y)) { this.x = x; this.y = y; return this },
zero() { this.x = this.y = 0; return this },
add(v, res = this) { res.x = this.x + v.x; res.y = this.y + v.y; return res },
scale(val, res = this) { res.x = this.x * val; res.y = this.y * val; return res },
get length() { return this.lengthSqr ** 0.5 },
set length(l) {
const len = this.lengthSqr;
len > 0 ? this.scale(l / len ** 0.5) : (this.x = l);
},
get lengthSqr() { return this.x * this.x + this.y * this.y },
};
canvas {background: #347;}
div {
position: absolute;
top: 150px;
left: 20px;
}
span { color: white; font-family: arial }
<canvas id="canvas" width="500" height="170"></canvas>
<div>
<button id="goA">Go A</button>
<button id="bumpA">Bump A</button>
<button id="goB">Go B</button>
<button id="bumpB">Bump B</button>
<span> Methods: A = Pull down B = Drag </span>
</div>
No limit
There are many variations on these methods, and the are many example on SO (I have written many answers in the subject. eg See demo snippet (bottom of answer) for example of drag method modification) .
Which method you use is very dependent on how you want the interaction to feel, there is no right or wrong method as game physics will is very different than real physics.

Knowing that acceleration (a) is the change in velocity (Δv) over the change in time (Δt), I'll check that.
Maybe with something like (pseudo):
float lastVelocity = 0;
bool isAccelerating = false;
Update()
{
float currentVelocity = rb.velocity;
if(currentVelocity > lastVelocity)
{
isAccelerating = true;
lastVelocity = currentVelocity;
}
else
{
isAccelerating = false;
}
}
Now you know when your "ship" is speedingUp, the only way to decrease the speed is caused by external forces (like gravity, or friction), depending of your setup, I'll deactivate those forces, or change the physicalMaterial that is causing the friction.

Related

Unity RotateAround Clamp Dead Zone Issue

For the past 3 days, I have tried numerous methods and read dozens of questions on forums in order to achieve the desired effect of rotating a camera on the Y axis using mouse rotations, and clamping the rotation within a certain range. The amount of rotation is dependent on how close the mouse is to the edge of the screen.
I was unable to find a solution, but I was able to learn enough to have my own honest attempt at it, and I came quite close. The prominent issue I faced was being able to properly clamp the RotateAround() inside of the desired range. This is because of eulerAngles being 0 - 360, and having the min and max of the clamp transition to the other side of the spectrum.
By clamping the rotation degrees before calling RotateAround, I was able to get the clamping to work, though I had to use Booleans to get them to work properly.
My current issue is that when the initial Y rotation(anchor) is slightly below 360, the camera fails to clamp on the left and will clamp back around to the right side of the range.
In all other situations, the clamping works just fine:
Initial Y rotation slightly above 0
Not within the dead zone
While debugging, I find that this is because rightOverFlag is true. When I set this bool to true manually in situations clamping normally works fine, the clamping will no longer work on the left.
I can't seem to find how this bool being true causes something like this to happen, so I am hoping some fresh eyes and seasoned advice can help me learn from this. Thank you.
public gameState gameState;
public openCams openCams;
private GameObject GameStateManager;
[SerializeField]
private GameObject Player;
[SerializeField]
private Camera cam;
// Rotation Variables
private Quaternion initialRotation;
private float initialYRotation = 0f;
[SerializeField]
private float angleRange;
private float anchorY = 0f;
public bool leftOverFlag = false;
public bool rightOverFlag = false;
void Start()
{
cam = Camera.main;
Cursor.lockState = CursorLockMode.None;
}
public void OrientControls()
{
initialRotation = Player.transform.rotation;
initialYRotation = Player.transform.eulerAngles.y;
anchorY = initialYRotation;
if (anchorY > 360)
{
anchorY -= 360;
}
rightOverFlag = false;
leftOverFlag = false;
if ((anchorY + angleRange) > 360)
{
rightOverFlag = true;
}
else if ((anchorY - angleRange) <= 0)
{
leftOverFlag = true;
}
}
void Update()
{
if (openCams.GetMonitorState() == false)
{
mousePos = Input.mousePosition;
float rotateDegrees = 0f;
//Debug.Log(Player.transform.eulerAngles.y);
// THERE IS CODE HERE THAT INCREASES ROTATE DEGREES BASED ON MOUSE POSITION. I removed it for the sake of readability, because it was quite long and unrelated to my issue.
float angleFromInitial = Quaternion.Angle(initialRotation, Player.transform.rotation);
float currentPlayerYRot = Player.transform.eulerAngles.y;
if (currentPlayerYRot > 360)
{
currentPlayerYRot -= 360;
}
bool currentRightOverageFlag = false;
bool currentLeftOverageFlag = false;
if (rightOverFlag)
{
if ((currentPlayerYRot) < (anchorY - (angleRange - 5))) //
{
currentRightOverageFlag = true;
}
}
if (leftOverFlag)
{
if ((currentPlayerYRot) > (anchorY + (angleRange + 5)))
{
currentLeftOverageFlag = true;
}
}
// !!! - For some reason, when rightOverFlag is enabled, the clamp does not work on the left side. In all other situations, the clamp works perfectly.- !!!
if (!currentLeftOverageFlag && !currentRightOverageFlag)
{
if (currentPlayerYRot < anchorY) // Regular
{
angleFromInitial *= -1;
}
}
else if (currentLeftOverageFlag && !currentRightOverageFlag)
{
if (currentPlayerYRot > anchorY) // If over the left line
{
angleFromInitial *= -1;
}
}
else if (!currentLeftOverageFlag && currentRightOverageFlag)
{
if (currentPlayerYRot > anchorY) // If over the right line
{
angleFromInitial *= -1;
}
}
else
{
Debug.Log("staticPersonController: ERROR => Cannot have current left and right overage flags enabled at the same time.");
}
currentLeftOverageFlag = false;
currentRightOverageFlag = false;
float newAngle = Mathf.Clamp(angleFromInitial + rotateDegrees, -angleRange, angleRange);
rotateDegrees = newAngle - angleFromInitial;
Player.transform.RotateAround(Player.transform.position, Vector3.up, rotateDegrees);
}
}
}
Assume you have these two variables set.
// This is the initial value of the player's Y rotation. Set only once.
float initialRotY = Player.transform.eulerAngles.y;
// This is the offset of the player's Y rotation based on the cursor position
// e.g. -90 when the cursor is on the left edge and 90 on the right
float mouseBasedRotY;
In the Update method, just do this.
Player.transform.rotation = Quaternion.Euler(0f, initialRotY + mouseBasedRotY, 0f);
You don't have to clamp the value, because the degree is always limited in [initialRotY-90, initialRotY+90]

Unity-I can't seem to lerp my GameObject's Color

I'm just trying to make an observer pattern program one object randomly changes color and causes a group of other objects to change the same color, but I wanted them to gradually change over 5 seconds. I'm trying lerp, but it just instantly swaps colors. I think it maybe has something to do with the lerp's starting color, because the main object is constantly shifting colors and new colors become old colors. So I need to think of how choose a starting color for the lerp. I'm not sure if this has to do with my the lerp isn't working, but it's what I'm considering. If anyone else has any suggestions, I would appreciate it. Thank you.
public class Subject : MonoBehaviour {
public float timer = 0.0f;
public GameObject[] observers;
float t = 0;
Color oldColor;
void Update () {
t += Time.deltaTime / 5.0f;
timer += Time.deltaTime;
if (timer >= 10f) {
Color newColor = new Color(Random.value, Random.value, Random.value, 1.0f);
GetComponent<Renderer>().material.color = newColor;
for (int i = 0; i < observers.Length; i++) {
observers[i].GetComponent<Renderer>().material.color = Color.Lerp(oldColor, newColor, t);
}
newColor=oldColor
timer = 0;
}
}
}
What Color.Lerp(Color a, Color b, float t) does is
Linearly interpolates between colors a and b by t
For example, Color.Lerp(Color.blue, Color.red, 0.5f) returns a half way interpolation between blue and red.
When you say Color.Lerp(oldColor, newColor, 5) it will return newColor, because
t is clamped between 0 and 1. When t is 0 returns a. When t is 1
returns b.
So 5 is the same thing as putting in 1.
So you will need to keep a t variable somewhere that starts at 0 and increments up to 1, you put into Lerp.
For example:
float t = 0;
private void Update()
{
t += Time.deltaTime / 5.0f; // Divided by 5 to make it 5 seconds.
this.GetComponent<Renderer>().color = Color.Lerp(oldColor, newColor, t);
}
https://docs.unity3d.com/ScriptReference/Color.Lerp.html
EDIT
Also whats happening in your code, assuming you reset t when timer hit
Timer >= 10.0f
Lerp once
Reset timer to 0
.
What you could do instead is something like this (i took out the observers and stuff just to make it easier to see)
float t, timer;
bool lerping = false;
Color newColor;
void Update()
{
t += Time.deltaTime / 5.0f;
timer += Time.deltaTime;
if (timer >= 10f)
{
lerping = true;
t = 0;
newColor = new Color(Random.value, Random.value, Random.value, 1.0f);
Debug.Log("Lerping!");
timer = 0;
}
if (lerping)
{
GetComponent<Renderer>().material.color = Color.Lerp(oldColor, newColor, t);
if (t >= 1.0f)
{
lerping = false;
Debug.Log("Stopped lerping");
}
}
}

acceleration-field for unity3D racing game, c#

for a motorcycle racing game I want some parts of the track to increase the speed of the motorcycle.
I have a public float for the speed, which shows the speed in the inspector.
I added a plane with a Collider, which is a trigger and has the tag "speedfield"
Now I thought, some simple code, attached to the motorcyclescript below, would do it:
void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag ("speedfield")) {
Speed = Speed + 50;
}
}
But nothing happens. I think I'm missing something obvious here. Hope you can help! Greetings from germany :)
void FixedUpdate (){
Inputs();
Engine();
}
void Inputs (){
Speed = rigid.velocity.magnitude * 3.6f;
//Freezing rotation by Z axis.
transform.eulerAngles = new Vector3(transform.eulerAngles.x, transform.eulerAngles.y, 0);
//If crashed...
if(!crashed){
if(!changingGear)
motorInput = Input.GetAxis("Vertical");
else
motorInput = Mathf.Clamp(Input.GetAxis("Vertical"), -1, 0);
steerInput = Input.GetAxis("Horizontal");
}else{
motorInput = 0;
steerInput = 0;
}
//Reverse bool
if(motorInput < 0 && transform.InverseTransformDirection(rigid.velocity).z < 0)
reversing = true;
else
reversing = false;
}
void Engine (){
//Steer Limit.
SteerAngle = Mathf.Lerp(defsteerAngle, highSpeedSteerAngle, (Speed / highSpeedSteerAngleAtSpeed));
FrontWheelCollider.steerAngle = SteerAngle * steerInput;
//Engine RPM.
EngineRPM = Mathf.Clamp((((Mathf.Abs((FrontWheelCollider.rpm + RearWheelCollider.rpm)) * gearShiftRate) + MinEngineRPM)) / (currentGear + 1), MinEngineRPM, MaxEngineRPM);
// Applying Motor Torque.
if(Speed > maxSpeed){
RearWheelCollider.motorTorque = 0;
}else if(!reversing && !changingGear){
RearWheelCollider.motorTorque = EngineTorque * Mathf.Clamp(motorInput, 0f, 1f) * engineTorqueCurve[currentGear].Evaluate(Speed);
}
if(reversing){
if(Speed < 10){
RearWheelCollider.motorTorque = (EngineTorque * motorInput) / 5f;
}else{
RearWheelCollider.motorTorque = 0;
}
}
}
You overriding speed variable:
void FixedUpdate (){
Inputs();
Engine();
}
void Inputs (){
Speed = rigid.velocity.magnitude * 3.6f;
...
}
change it to:
[SerializeField] float speedMultiplier = 3.6f;
[SerializeField] float speedMultiplierChange = 2;
void Inputs (){
Speed = rigid.velocity.magnitude * speedMultiplier;
...
}
and
void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag ("speedfield")) {
speedMultiplier += speedMultiplierChange;
}
}
Make sure that all items in this checklist are satisfied:
The motorcycle has a Rigidbody component.
The plane doesn't have a Rigidbody component (it's useless and it could slow things down).
The Mesh Collider of the plane has Convex and isTrigger set to true.
The OnTriggerEnter method is in the motorcycle script.
The thickness of the plane trigger is wide enough: if the motorcycle speed is too high, it can pass thru the plane without the physics engine being able to register the trigger.
If this is the case, try to set the Collision Detection of the motorcycle Rigidbody to Continuous and/or decrease somewhat the Fixed Timestep in the Time project settings.
Most probably the problem is due to the last two bullet points.

2D projectile trajectory prediction (unity3d)

(Using unity3d 4.3 2d, it uses box2d like physics).
I have problems with predicting trajectory
I'm using:
Vector2 startPos;
float power = 10.0f;
float interval = 1/30.0f;
GameObject[] ind;
void Start (){
transform.rigidbody2D.isKinematic = true;
ind = new GameObject[dots];
for(int i = 0; i<dots; i++){
GameObject dot = (GameObject)Instantiate(Dot);
dot.renderer.enabled = false;
ind[i] = dot;
}
}
void Update (){
if(shot) return;
if(Input.GetAxis("Fire1") == 1){
if(!aiming){
aiming = true;
startPos = Input.mousePosition;
ShowPath();
}
else{
CalculatePath();
}
}
else if(aiming && !shot){
transform.rigidbody2D.isKinematic = false;
transform.rigidbody2D.AddForce(GetForce(Input.mous ePosition));
shot = true;
aiming = false;
HidePath();
}
}
Vector2 GetForce(Vector3 mouse){
return (new Vector2(startPos.x, startPos.y)- new Vector2(mouse.x, mouse.y))*power;
}
void CalculatePath(){
ind[0].transform.position = transform.position; //set frist dot to ball position
Vector2 vel = GetForce(Input.mousePosition); //get velocity
for(int i = 1; i < dots; i++){
ind[i].renderer.enabled = true; //make them visible
Vector3 point = PathPoint(transform.position, vel, i); //get position of the dot
point.z = -1.0f;
ind[i].transform.position = point;
}
}
Vector2 PathPoint(Vector2 startP, Vector2 startVel, int n){
//Standard formula for trajectory prediction
float t = interval;
Vector2 stepVelocity = t*startVel;
Vector2 StepGravity = t*t*Physics.gravity;
Vector2 whattoreturn = ((startP + (n * stepVelocity)+(n*n+n)*StepGravity) * 0.5f);
return whattoreturn;
}
Using this, I get wrong trajectory.
1. It's like gravity doesn't drag trajectory down at all, and yes i know that gravity is weak because:
t*t*Physics.gravity = 0.03^2 * vector2(0, -9.8) = vector2(0, -0.00882)
But that is the formula :S
2. Since gravity is low, velocity is too strong.
Here is the video:
http://tinypic.com/player.php?v=1z50w3m&s=5
Trajectory formula form:
http://www.iforce2d.net/b2dtut/projected-trajectory
What should I do?
I found that if I set
StepGravity to something stronger like (0, -0.1)
and devide startVel by 8
I get nearly right trajectory, but i don't want that, I need true trajectory path.
Users from answer.unity3d.com said I should ask here, because here is a bigger group of mathematical coders.
And I searched a lot about this problem (that how I found that formula).
you're only calculating the effect of gravity over 1/30th of a second for each step - you need to do it cumulatively. Step 1 should end with a velocity of 0.09G, Step 2 with .18G, step3 with .27G etc.
Here's a very simple example that draws the ballistic trajectory based on start velocity and a supplied time:
using UnityEngine;
using System.Collections;
public class grav : MonoBehaviour {
public Vector3 StartVelocity;
public float PredictionTime;
private Vector3 G;
void OnDrawGizmos()
{
if (G == Vector3.zero)
{
// a hacky way of making sure this gets initialized in editor too...
// this assumes 60 samples / sec
G = new Vector3(0,-9.8f,0) / 360f;
}
Vector3 momentum = StartVelocity;
Vector3 pos = gameObject.transform.position;
Vector3 last = gameObject.transform.position;
for (int i = 0; i < (int) (PredictionTime * 60); i++)
{
momentum += G;
pos += momentum;
Gizmos.DrawLine(last, pos);
last = pos;
}
}
}
In you version you'd want draw your dots where I'm drawing the Gizmo, but it's the same idea unless I'm misunderstanding your problem.

Unity3D Third Person Controller and Animations

My problem is that a custom walk animation I have set in the third person controller of Unity3D is not shown.
The animation is imported from a FBX file with the model#walk.fbx structure. I know the animation works, because if I use it as the idle animation it shows. On the other hand, it also doesn't seem to be an issue with the controller script, as it works fine with the prototype character.
It seems that the animation being played is the one selected in the Animation component (which has 'Play Automatically' selected). Any attempts to change the animation from the third person controller work for the prototype character, but not for mine.
I don't have nor want run and jump animations. With the prototype character I set the walk animation on each of those with no adverse effects. The controller doesn't turn off animation this way, seeing that there are no log entries in the console from the third person controller script. The relevant line with the CrossFade call is getting called.
Any clues as to where I could look next? Is it more likely an issue with the controller, or with the animation? Something else completely?
Update: Below is the code of my controller. It works fine when I use the sample model of the construction worker that is provided with Unity. The lines with _animation.CrossFade are getting called at the expected times. Using Play or Blend instead doesn't help. There are no errors logged in the console.
For our custom animations however it doesn't work. I am now suspecting the issues lies with the model. Unfortunately I am not at liberty to share a sample of that model. I've asked the animator for further details on how he created the FBX export. Are there any specific settings he needs to use for the model to work in Unity? It remains odd though that the animations do work if I add them indepently to the scene.
// Require a character controller to be attached to the same game object
#script RequireComponent(CharacterController)
public var idleAnimation : AnimationClip;
public var walkAnimation : AnimationClip;
public var walkMaxAnimationSpeed : float = 0.75;
private var _animation : Animation;
enum CharacterState {
Idle = 0,
Walking = 1,
}
private var _characterState : CharacterState;
// The speed when walking
var walkSpeed = 2.0;
var speedSmoothing = 10.0;
var rotateSpeed = 500.0;
var targetPrecision = 5;
var targetMaxDistance = 200;
// The camera doesnt start following the target immediately but waits for a split second to avoid too much waving around.
private var lockCameraTimer = 0.0;
// The current move direction in x-z
private var moveDirection = Vector3.zero;
// The current x-z move speed
private var moveSpeed = 0.0;
// The last collision flags returned from controller.Move
private var collisionFlags : CollisionFlags;
// Are we moving backwards (This locks the camera to not do a 180 degree spin)
private var movingBack = false;
// Is the user pressing any keys?
private var isMoving = false;
private var isControllable = true;
private var isTargetting : boolean = false;
private var targetPoint : Vector3 = Vector3.zero;
function Awake () {
moveDirection = transform.TransformDirection(Vector3.forward);
_animation = GetComponent(Animation);
if(!_animation)
Debug.Log("The character you would like to control doesn't have animations. Moving her might look weird.");
if(!idleAnimation) {
_animation = null;
Debug.Log("No idle animation found. Turning off animations.");
}
//_animation[idleAnimation.name] = idleAnimation;
if(!walkAnimation) {
_animation = null;
Debug.Log("No walk animation found. Turning off animations.");
}
//_animation[walkAnimation.name] = walkAnimation;
}
function UpdateSmoothedMovementDirection () {
var cameraTransform = Camera.main.transform;
// Forward vector relative to the camera along the x-z plane
var forward = cameraTransform.TransformDirection(Vector3.forward);
forward.y = 0;
forward = forward.normalized;
// Right vector relative to the camera
// Always orthogonal to the forward vector
var right = Vector3(forward.z, 0, -forward.x);
var v = Input.GetAxisRaw("Vertical");
var h = Input.GetAxisRaw("Horizontal");
// Are we moving backwards or looking backwards
if (v < -0.2)
movingBack = true;
else
movingBack = false;
var wasMoving = isMoving;
isMoving = Mathf.Abs (h) > 0.1 || Mathf.Abs (v) > 0.1;
// Target direction relative to the camera
var targetDirection = h * right + v * forward;
// Lock camera for short period when transitioning moving & standing still
lockCameraTimer += Time.deltaTime;
if (isMoving != wasMoving)
lockCameraTimer = 0.0;
// We store speed and direction seperately,
// so that when the character stands still we still have a valid forward direction
// moveDirection is always normalized, and we only update it if there is user input.
if (targetDirection != Vector3.zero) {
// If we are really slow, just snap to the target direction
if (moveSpeed < walkSpeed * 0.9) {
moveDirection = targetDirection.normalized;
}
// Otherwise smoothly turn towards it
else {
moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);
moveDirection = moveDirection.normalized;
}
}
// Smooth the speed based on the current target direction
var curSmooth = speedSmoothing * Time.deltaTime;
// Choose target speed
//* We want to support analog input but make sure you cant walk faster diagonally than just forward or sideways
var targetSpeed = Mathf.Min(targetDirection.magnitude, 1.0);
_characterState = CharacterState.Idle;
// Pick speed modifier
targetSpeed *= walkSpeed;
_characterState = CharacterState.Walking;
moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
}
function UpdateTargettedMovementDirection () {
var cameraTransform = Camera.main.transform;
var wasMoving = isMoving;
isMoving = true;//Mathf.Abs (h) > 0.1 || Mathf.Abs (v) > 0.1;
// Target direction relative to the camera
// var targetDirection = h * right + v * forward;
var targetDirection = Vector3.zero;
targetDirection.x = targetPoint.x - transform.position.x;
targetDirection.z = targetPoint.z - transform.position.z;
targetDirection = targetDirection.normalized;
//Debug.Log("Target direction is " + targetDirection);
// Lock camera for short period when transitioning moving & standing still
lockCameraTimer += Time.deltaTime;
if (isMoving != wasMoving)
lockCameraTimer = 0.0;
// We store speed and direction seperately,
// so that when the character stands still we still have a valid forward direction
// moveDirection is always normalized, and we only update it if there is user input.
if (targetDirection != Vector3.zero) {
// If we are really slow, just snap to the target direction
if (moveSpeed < walkSpeed * 0.9) {
moveDirection = targetDirection.normalized;
}
// Otherwise smoothly turn towards it
else {
moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);
moveDirection = moveDirection.normalized;
}
}
// Smooth the speed based on the current target direction
var curSmooth = speedSmoothing * Time.deltaTime;
// Choose target speed
//* We want to support analog input but make sure you cant walk faster diagonally than just forward or sideways
var targetSpeed = Mathf.Min(targetDirection.magnitude, 1.0);
_characterState = CharacterState.Idle;
// Pick speed modifier
targetSpeed *= walkSpeed;
_characterState = CharacterState.Walking;
moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
}
function Update() {
if (!isControllable) {
// kill all inputs if not controllable.
Input.ResetInputAxes();
}
var distance : float = 0;
if (Input.GetMouseButtonUp(0)) {
if (isTargetting) {
isTargetting = false;
// Debug.Log("Stopped moving");
FaceCamera();
} else {
var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
var layerMask = 1 << 8; // Terrain is layer 8
var hit : RaycastHit;
Physics.Raycast(Camera.main.transform.position, ray.direction, hit, 1000, layerMask);
distance = Vector3.Distance(transform.position, hit.point);
if (distance <= targetMaxDistance && hit.point != Vector3.zero) {
targetPoint = hit.point;
isTargetting = true;
// Debug.Log("Mouse up at hit " + hit.point + " at distance " + distance);
} else {
isTargetting = false;
// Debug.Log("Ignored mouse up at hit " + hit.point + " at distance " + distance);
}
}
}
if (isTargetting) {
// Debug.Log("Moving to " + targetPoint);
distance = Vector3.Distance(transform.position, targetPoint);
if (distance < targetPrecision) {
// Debug.Log("Reached point " + targetPoint + " at distance " + distance);
isTargetting = false;
FaceCamera();
} else {
UpdateTargettedMovementDirection();
}
} else {
UpdateSmoothedMovementDirection();
}
// Calculate actual motion
var movement = moveDirection * moveSpeed;
movement *= Time.deltaTime;
// Move the controller
var controller : CharacterController = GetComponent(CharacterController);
collisionFlags = controller.Move(movement);
// ANIMATION sector
if (_animation) {
if (controller.velocity.sqrMagnitude < 0.1) {
_animation.CrossFade(idleAnimation.name);
} else {
//_animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, walkMaxAnimationSpeed);
_animation.CrossFade(walkAnimation.name);
}
} else {
Debug.Log("Animation is null!");
}
// ANIMATION sector
// Set rotation to the move direction
transform.rotation = Quaternion.LookRotation(moveDirection);
}
function OnControllerColliderHit (hit : ControllerColliderHit ) {
// Debug.DrawRay(hit.point, hit.normal);
if (hit.moveDirection.y > 0.01)
return;
}
function GetSpeed () {
return moveSpeed;
}
function GetDirection () {
return moveDirection;
}
function IsMovingBackwards () {
return movingBack;
}
function GetLockCameraTimer () {
return lockCameraTimer;
}
function IsMoving () : boolean {
return Mathf.Abs(Input.GetAxisRaw("Vertical")) + Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0.5;
}
function Reset () {
gameObject.tag = "Player";
}
function FaceCamera() {
var cameraTransform = Camera.main.transform;
// Forward vector relative to the camera along the x-z plane
var forward = cameraTransform.TransformDirection(Vector3.forward);
forward.y = 0;
forward = forward.normalized;
moveDirection = -forward;
}
Update 2: The settings used to create the animations are in these screenshots. Are they correct?
Not (yet :-) an answer but I need more space and images.
Since 3.5 Unity3d the way to handle imports based on animation#model.fbx notation has changed and some people reported trouble (s. for example BIG Unity 3.5.0f1 problem with Skinned Rig - Major FAIL). The problem arises when a 2nd root bone comes to play but I could solve this by dragging the animations to the model file prefab (most of my animations are contained in the model and the remaining 2 are no big pain).
Just to be really sure that I understood your answer in the comments section right, you have something like this:
That means:
Within the character model the animations array contains all animations
The number of bones and all names are exactly like in your prototype character
If you open an animation view, you can see a read-only list of all animations of the character selected in hierarchy view
Assuming this is fine some more suggestions:
I can't see any place in the code where you set WrapMode.Loop or animation speed. Are you sure it is configured as Loop in inspector and is not overwritten somewhere.
CrossFade with no parameters assumes 0.3 seconds
What happens after the cross fade call? Does the DefaulTake stops playing?
Can you drag the Walk animation as default
Do you use different animation layers?
What output do you get when you set up Debug.Log ("Walk: " + player.animation.IsPlaying ("Walk"));
[Update]
Let's look at the import settings. Do you have Split Animations
checked and are there maybe wrong values entered at Start and
End like 1-1?
Regarding Toggling of IsPlaying: Please create more output about the AnimationState properties. Most notable speed, length, enabled, weight, ... I have a suspicion that the animation is too short or played too fast.
A walk animation that is no bone animation sounds a bit strange. On the other hand console would cry out loud if bone names are not matching. So I assume there is no problem now.
That is strange, can you post the source&demo?
You can try to check:
Check if the animation names and properties are set correctly in the editor.
Create an object without the controller and make sure the animations were imported correctly.