# How To Rotate Player Relative to Slope Angle

I’ve been coding a sonic game here on PlayCanvas, and I got mostly everything done for my basic first step. Sonic can run through loops. His speed changes based on the angle he’s currently at (an angle variable is used). The method I’ve been using for rotation is simple. I check what tag the collision has, and the player adjusts it’s angle to the ground. (Ex. if player collides with collision with ‘turn45’, it turns 45 angles). Now with a slope, this isn’t as intuitive, as you can’t have separate blocks checking what angle the player should be at. I want to make it so that the player rotates accordingly to the slope angle. Basically, the angle variables changes depending on the angle of the slope. My problem is that I can’t figure out a good way to do this. Does anyone know how I can get my player to rotate on a slope accordingly, without using tags?

Code:

``````var PlayerScript = pc.createScript('playerScript');

//Variables
var xsp = 0;
var ysp = 0;
var gsp = 0;
var acc = 0.126875;
var dec = 0.5;
var frc = acc;
var limit = 14;
var goingRight;
var angle = 0;
var grv = 0.81875;
var grounded = false;
var pcos = true;
var air = 0.19375;
var jmp = 16.5;
var checkAir = true;
var isJumping = false;

//Animations
PlayerScript.attributes.add("main", { type: 'entity', title: 'Main' });
PlayerScript.attributes.add("idle", { type: 'entity', title: 'Idle' });
PlayerScript.attributes.add("walk", { type: 'entity', title: 'Walk' });
PlayerScript.attributes.add("run", { type: 'entity', title: 'Run' });
PlayerScript.attributes.add("jump", { type: 'entity', title: 'Jump' });

//Layers
PlayerScript.attributes.add("frontLayer", { type: 'entity', title: 'Front Layer' });
PlayerScript.attributes.add("backlayer", { type: 'entity', title: 'Back Layer' });
var frontLayer;
var backLayer;

// initialize code called once per entity
PlayerScript.prototype.initialize = function() {
this.app.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown, this);
this.app.keyboard.on(pc.EVENT_KEYUP, this.onKeyUp, this);
this.entity.collision.on('collisionstart', this.onCollisionStart, this);
this.entity.collision.on('contact', this.onContact, this);

this.idle.enabled = true;
this.walk.enabled = false;
this.run.enabled = false;
this.jump.enabled = false;

frontLayer = this.app.root.findByName('Front Layer');
backLayer = this.app.root.findByName('Back Layer');
};

// update code called every frame
PlayerScript.prototype.update = function(dt) {
//Main
this.entity.rigidbody.linearVelocity = new pc.Vec3(xsp, ysp, 0);
if (pcos === true) {
xsp = gsp * this.cosine(angle);
if (grounded === true) {
ysp = gsp * this.sine(angle);
}
}

//Functions
this.movement();
this.friction();
this.gravity();
this.animation();
this.flip();
};

PlayerScript.prototype.movement = function() {
//Main Movement Check
if (checkAir === false) {
if (this.app.keyboard.isPressed(pc.KEY_RIGHT)) {
gsp += acc;
goingRight = true;
}
if (this.app.keyboard.isPressed(pc.KEY_LEFT)) {
gsp -= acc;
goingRight = false;
}
if ((!this.app.keyboard.isPressed(pc.KEY_RIGHT) && !this.app.keyboard.isPressed(pc.KEY_LEFT)) || (this.app.keyboard.isPressed(pc.KEY_RIGHT) && this.app.keyboard.isPressed(pc.KEY_LEFT))) {
goingRight = null;
}
}
if (checkAir === true) {
if (this.app.keyboard.isPressed(pc.KEY_RIGHT)) {
gsp += air;
goingRight = true;
}
if (this.app.keyboard.isPressed(pc.KEY_LEFT)) {
gsp -= air;
goingRight = false;
}

//Air Drag
if (ysp > 0 && ysp < 14) {
if (Math.abs(xsp) >= 0.125) {
xsp = xsp * 0.96875;
}
}
}

//Jump
if (this.app.keyboard.wasPressed(pc.KEY_A) && grounded === true) {
ysp += jmp;
grounded = false;
isJumping = true;
}

//Limits
if (gsp > limit || gsp < -limit) {
if (gsp > limit) {
gsp = limit;
}
if (gsp < -limit) {
gsp = -limit;
}
}
};

PlayerScript.prototype.friction = function() {
//Main
if (gsp > 0 && goingRight === false) {
gsp = gsp - dec;
}
if (gsp < 0 && goingRight === true) {
gsp = gsp + dec;
}
if ((gsp > 0 || gsp < 0) && goingRight === null) {
if (gsp > 0) {
gsp -= frc;
}
if (gsp < 0) {
gsp += frc;
}
if (gsp < frc && gsp > -frc && gsp !== 0) {
gsp = 0;
if (gsp === 0) {
gsp = gsp;
}
}
}
};

PlayerScript.prototype.gravity = function() {
//Main
if (grounded === false) {
ysp -= grv;
if (ysp < -26) {
ysp = -16;
}
checkAir = true;
}
if (grounded === true && ysp !== 0) {
ysp = ysp;
if (ysp === 0) {
ysp = ysp;
}
checkAir = false;
}
};

PlayerScript.prototype.animation = function() {
if ((gsp < 0.01 && gsp > -0.01) && isJumping === false) {
this.idle.enabled = true;
this.walk.enabled = false;
this.run.enabled = false;
this.jump.enabled = false;
}
if (((gsp > 0.01 && gsp < limit) || (gsp < -0.01 && gsp > -limit)) && isJumping === false) {
this.idle.enabled = false;
this.walk.enabled = true;
this.run.enabled = false;
this.jump.enabled = false;
}
if ((gsp >= limit || gsp <= -limit) && isJumping === false) {
this.idle.enabled = false;
this.walk.enabled = false;
this.run.enabled = true;
this.jump.enabled = false;
}
if (isJumping === true) {
this.idle.enabled = false;
this.walk.enabled = false;
this.run.enabled = false;
this.jump.enabled = true;
}
};

PlayerScript.prototype.flip = function() {
if (goingRight === true && gsp > 0) {
this.main.setLocalScale(1, 1, 1);
}
if (goingRight === false && gsp < 0) {
this.main.setLocalScale(-1, 1, 1);
}
};

PlayerScript.prototype.onCollisionStart = function(result) {
//Check Ground
if (result.other.tags.has('ground')) {
grounded = true;
checkAir = false;
isJumping = false;
}

//Rotations
if (result.other.tags.has('turn0')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 0);
}
if (result.other.tags.has('turn45')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 45);
}
if (result.other.tags.has('turn90')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 90);
}
if (result.other.tags.has('turn135')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 135);
}
if (result.other.tags.has('turn180')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 180);
}
if (result.other.tags.has('turn225')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 225);
}
if (result.other.tags.has('turn270')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 270);
}
if (result.other.tags.has('turn315')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 315);
}

//Switch Layers
if (result.other.tags.has('switchLayer')) {
frontLayer.enabled = !frontLayer.enabled;
backLayer.enabled = !backLayer.enabled;
}
};

PlayerScript.prototype.onContact = function(result) {
//Rotations
if (result.other.tags.has('turn0')) {
angle = 0;
pcos = true;
}
if (result.other.tags.has('turn45')) {
angle = 45;
pcos = true;
}
if (result.other.tags.has('turn90')) {
angle = 90;
pcos = true;
}
if (result.other.tags.has('turn135')) {
angle = 144;
pcos = true;
}
if (result.other.tags.has('turn180')) {
angle = 0;
pcos = false;
xsp = gsp * -this.cosine(angle);
ysp = gsp * -this.sine(angle);
}
if (result.other.tags.has('turn225')) {
angle = -126;
pcos = true;
}
if (result.other.tags.has('turn270')) {
angle = -90;
pcos = true;
}
if (result.other.tags.has('turn315')) {
angle = -45;
pcos = true;
}
};

PlayerScript.prototype.sine = function(value) {
var accuracy = 0.0001, denominator, sinx, sinval;

value = value * (3.142 / 180.0);

var x1 = value;

//Maps the Sum along the Series
sinx = value;

//Holds the actual value of sin(value)
sinval = Math.sin(value);
var i = 1;
do {
denominator = 2 * i * (2 * i + 1);
x1 = -x1 * value * value / denominator;
sinx = sinx + x1;
i = i + 1;
} while (accuracy <= sinval - sinx);

return sinx;
};

PlayerScript.prototype.cosine = function(value) {
var accuracy = 0.0001, x1, denominator, cosx, cosval;

value = value * (3.142 / 180.0);
x1 = 1;

//Maps the Sum along the Series
cosx = x1;

//Holds the actual value of cosine(value)
cosval = Math.cos(value);
var i = 1;
do {
denominator = 2 * i * (2 * i - 1);
x1 = -x1 * value * value / denominator;
cosx = cosx + x1;
i = i + 1;
} while (accuracy <= cosval - cosx);

return cosx;
};

// inherit your script state here
// PlayerScript.prototype.swap = function(old) { };

// http://developer.playcanvas.com/en/user-manual/scripting/
``````

Anyone knows how to rotate the player accordingly to the slope’s angle?

Hi @DevPlex01,

You can raycast from the player’s position to the ground, then the raycast result contains a normal as well.

You can use that normal to rotate your player relative to the surface.

https://developer.playcanvas.com/en/api/pc.RaycastResult.html

1 Like

Here is some code on how to use that normal to rotate an entity:

``````function setMat4Forward(mat4, forward, up) {
var x = this.x;
var y = this.y;
var z = this.z;

// Inverse the forward direction as +z is pointing backwards due to the coordinate system
z.copy(forward).scale(-1);
y.copy(up).normalize();
x.cross(y, z).normalize();
y.cross(z, x);

var r = mat4.data;

r[0] = x.x;
r[1] = x.y;
r[2] = x.z;
r[3] = 0;
r[4] = y.x;
r[5] = y.y;
r[6] = y.z;
r[7] = 0;
r[8] = z.x;
r[9] = z.y;
r[10] = z.z;
r[11] = 0;
r[15] = 1;

return mat4;
};

this.matrix = new pc.Mat4();
this.quat = new pc.Quat();

setMat4Forward(this.matrix, normal, pc.Vec3.UP);
this.quat.setFromMat4(this.matrix);
newEntity.setRotation(this.quat);
``````

I’m a bit confused on what a Mat4 is. Also, do I create a new script, or do I add this to a currently existing script? Sorry for my confusion. I’m just not used to the code used in the example.

Unrelated: This is amazing and I love it.

I’ll try the script and see what hapens.

lol yes. It’s supposed to happen. I’m still have to add the slope factors in. I will add them once I get the rotation down (smooth rotation, not the tag check rotation).

Here is an example of using the raycast normal result to align an object:

https://playcanvas.com/editor/scene/927496

1 Like

Another question. Um, do you need the `event` in `this.doRaycast(event);` ?

For this specific application yes, since the raycast uses the mouse x and y coords.

But for aligning the hit marker no, it’s not used there.

Ok, so I would replace the from with the player entity in my game, and the to variable to where?

Also, sorry for asking so many questions. I just never done something like this before.

Excuse me, but what would the from and to variables be in a game like mine? I’m assuming in my game, the from is the player, but what is the to?

The to position I imagine should be a position below your player, into the ground.

``````var from = this.player.getPosition();

// this in a Sonic-like game will try and get a point 10 units away/below your player's pos
var to = new pc.Vec3().copy(this.player.up).scale(-10);
````````

I guess that makes sense. I’ll try to test out the code now.

Ok, so I tried it out, and sonic just stays in the same rotation. I’m not sure if i did something wrong, but here’s what I did:

``````var PlayerScript = pc.createScript('playerScript');

//Variables
var xsp = 0;
var ysp = 0;
var gsp = 0;
var acc = 0.126875;
var dec = 0.5;
var frc = acc;
var limit = 14;
var goingRight;
var angle = 0;
var grv = 0.81875;
var grounded = false;
var pcos = true;
var air = 0.19375;
var jmp = 16.5;
var checkAir = true;
var isJumping = false;

//Animations
PlayerScript.attributes.add("main", { type: 'entity', title: 'Main' });
PlayerScript.attributes.add("idle", { type: 'entity', title: 'Idle' });
PlayerScript.attributes.add("walk", { type: 'entity', title: 'Walk' });
PlayerScript.attributes.add("run", { type: 'entity', title: 'Run' });
PlayerScript.attributes.add("jump", { type: 'entity', title: 'Jump' });

//Layers
var frontLayer;
var backLayer;

// initialize code called once per entity
PlayerScript.prototype.initialize = function() {
this.app.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown, this);
this.app.keyboard.on(pc.EVENT_KEYUP, this.onKeyUp, this);
this.entity.collision.on('collisionstart', this.onCollisionStart, this);
this.entity.collision.on('contact', this.onContact, this);

this.idle.enabled = true;
this.walk.enabled = false;
this.run.enabled = false;
this.jump.enabled = false;

frontLayer = this.app.root.findByName('Front Layer');
backLayer = this.app.root.findByName('Back Layer');

this.vec = new pc.Vec3();

this.matrix = new pc.Mat4();
this.quat = new pc.Quat();

this.x = new pc.Vec3();
this.y = new pc.Vec3();
this.z = new pc.Vec3();
};

// update code called every frame
PlayerScript.prototype.update = function(dt) {
//Main
this.entity.rigidbody.linearVelocity = new pc.Vec3(xsp, ysp, 0);
if (pcos === true) {
xsp = gsp * this.cosine(angle);
if (grounded === true) {
ysp = gsp * this.sine(angle);
}
}

//Functions
this.movement();
this.friction();
this.gravity();
this.animation();
this.flip();
this.doRayCast();
};

PlayerScript.prototype.movement = function() {
//Main Movement Check
if (checkAir === false) {
if (this.app.keyboard.isPressed(pc.KEY_RIGHT)) {
gsp += acc;
goingRight = true;
}
if (this.app.keyboard.isPressed(pc.KEY_LEFT)) {
gsp -= acc;
goingRight = false;
}
if ((!this.app.keyboard.isPressed(pc.KEY_RIGHT) && !this.app.keyboard.isPressed(pc.KEY_LEFT)) || (this.app.keyboard.isPressed(pc.KEY_RIGHT) && this.app.keyboard.isPressed(pc.KEY_LEFT))) {
goingRight = null;
}
}
if (checkAir === true) {
if (this.app.keyboard.isPressed(pc.KEY_RIGHT)) {
gsp += air;
goingRight = true;
}
if (this.app.keyboard.isPressed(pc.KEY_LEFT)) {
gsp -= air;
goingRight = false;
}

//Air Drag
if (ysp > 0 && ysp < 14) {
if (Math.abs(xsp) >= 0.125) {
xsp = xsp * 0.96875;
}
}
}

//Jump
if (this.app.keyboard.wasPressed(pc.KEY_A) && grounded === true) {
ysp += jmp;
grounded = false;
isJumping = true;
}

//Limits
if (gsp > limit || gsp < -limit) {
if (gsp > limit) {
gsp = limit;
}
if (gsp < -limit) {
gsp = -limit;
}
}
};

PlayerScript.prototype.friction = function() {
//Main
if (gsp > 0 && goingRight === false) {
gsp = gsp - dec;
}
if (gsp < 0 && goingRight === true) {
gsp = gsp + dec;
}
if ((gsp > 0 || gsp < 0) && goingRight === null) {
if (gsp > 0) {
gsp -= frc;
}
if (gsp < 0) {
gsp += frc;
}
if (gsp < frc && gsp > -frc && gsp !== 0) {
gsp = 0;
if (gsp === 0) {
gsp = gsp;
}
}
}
};

PlayerScript.prototype.gravity = function() {
//Main
if (grounded === false) {
ysp -= grv;
if (ysp < -26) {
ysp = -16;
}
checkAir = true;
}
if (grounded === true && ysp !== 0) {
ysp = ysp;
if (ysp === 0) {
ysp = ysp;
}
checkAir = false;
}
};

PlayerScript.prototype.animation = function() {
if ((gsp < 0.01 && gsp > -0.01) && isJumping === false) {
this.idle.enabled = true;
this.walk.enabled = false;
this.run.enabled = false;
this.jump.enabled = false;
}
if (((gsp > 0.01 && gsp < limit) || (gsp < -0.01 && gsp > -limit)) && isJumping === false) {
this.idle.enabled = false;
this.walk.enabled = true;
this.run.enabled = false;
this.jump.enabled = false;
}
if ((gsp >= limit || gsp <= -limit) && isJumping === false) {
this.idle.enabled = false;
this.walk.enabled = false;
this.run.enabled = true;
this.jump.enabled = false;
}
if (isJumping === true) {
this.idle.enabled = false;
this.walk.enabled = false;
this.run.enabled = false;
this.jump.enabled = true;
}
};

PlayerScript.prototype.flip = function() {
if (goingRight === true && gsp > 0) {
this.main.setLocalScale(1, 1, 1);
}
if (goingRight === false && gsp < 0) {
this.main.setLocalScale(-1, 1, 1);
}
};

PlayerScript.prototype.onCollisionStart = function(result) {
//Check Ground
if (result.other.tags.has('ground')) {
grounded = true;
checkAir = false;
isJumping = false;
}

//Rotations
/*if (result.other.tags.has('turn0')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 0);
}
if (result.other.tags.has('turn45')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 45);
}
if (result.other.tags.has('turn90')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 90);
}
if (result.other.tags.has('turn135')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 135);
}
if (result.other.tags.has('turn180')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 180);
}
if (result.other.tags.has('turn225')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 225);
}
if (result.other.tags.has('turn270')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 270);
}
if (result.other.tags.has('turn315')) {
this.entity.rigidbody.teleport(this.entity.getPosition().x, this.entity.getPosition().y, this.entity.getPosition().z, 0, 0, 315);
}*/

//Switch Layers
if (result.other.tags.has('switchLayer')) {
frontLayer.enabled = !frontLayer.enabled;
backLayer.enabled = !backLayer.enabled;
}
};

PlayerScript.prototype.onContact = function(result) {
//Rotations
if (result.other.tags.has('turn0')) {
angle = 0;
pcos = true;
}
if (result.other.tags.has('turn45')) {
angle = 45;
pcos = true;
}
if (result.other.tags.has('turn90')) {
angle = 90;
pcos = true;
}
if (result.other.tags.has('turn135')) {
angle = 144;
pcos = true;
}
if (result.other.tags.has('turn180')) {
angle = 0;
pcos = false;
xsp = gsp * -this.cosine(angle);
ysp = gsp * -this.sine(angle);
}
if (result.other.tags.has('turn225')) {
angle = -126;
pcos = true;
}
if (result.other.tags.has('turn270')) {
angle = -90;
pcos = true;
}
if (result.other.tags.has('turn315')) {
angle = -45;
pcos = true;
}
};

PlayerScript.prototype.sine = function(value) {
var accuracy = 0.0001, denominator, sinx, sinval;

value = value * (3.142 / 180.0);

var x1 = value;

//Maps the Sum along the Series
sinx = value;

//Holds the actual value of sin(value)
sinval = Math.sin(value);
var i = 1;
do {
denominator = 2 * i * (2 * i + 1);
x1 = -x1 * value * value / denominator;
sinx = sinx + x1;
i = i + 1;
} while (accuracy <= sinval - sinx);

return sinx;
};

PlayerScript.prototype.cosine = function(value) {
var accuracy = 0.0001, x1, denominator, cosx, cosval;

value = value * (3.142 / 180.0);
x1 = 1;

//Maps the Sum along the Series
cosx = x1;

//Holds the actual value of cosine(value)
cosval = Math.cos(value);
var i = 1;
do {
denominator = 2 * i * (2 * i - 1);
x1 = -x1 * value * value / denominator;
cosx = cosx + x1;
i = i + 1;
} while (accuracy <= cosval - cosx);

return cosx;
};

PlayerScript.prototype.doRayCast = function(screenPosition) {
var from = this.entity.getPosition();
var to = new pc.Vec3().copy(this.entity.up).scale(-10);

var result = this.app.systems.rigidbody.raycastFirst(from, to);

if (result) {
this.setMat4Forward(this.matrix, result.normal, pc.Vec3.UP);
this.quat.setFromMat4(this.matrix);
this.entity.setRotation(this.quat);
}
};

PlayerScript.prototype.setMat4Forward = function(mat4, forward, up) {
var x = this.x;
var y = this.y;
var z = this.z;

// Inverse the forward direction as +z is pointing backwards due to the coordinate system
z.copy(forward).scale(-1);
y.copy(up).normalize();
x.cross(y, z).normalize();
y.cross(z, x);

var r = mat4.data;

r[0] = x.x;
r[1] = x.y;
r[2] = x.z;
r[3] = 0;
r[4] = y.x;
r[5] = y.y;
r[6] = y.z;
r[7] = 0;
r[8] = z.x;
r[9] = z.y;
r[10] = z.z;
r[11] = 0;
r[15] = 1;

return mat4;
};

// inherit your script state here
// PlayerScript.prototype.swap = function(old) { };