It is complicated, to have something acceptable it has taken me a long time and look at many examples. In the end, I decided to go use the physics engine.
Anyway, here is my implementation without the physics engine:
first, create a new class to manage the colliders (sory many comments are in spanish, but you can use google translator)
/* Coded by Diego Toritto, feel free to make modifications and use it in your projects,
give credits if possible, if not, it doesn't matter.
*/
export enum HitOn {
characterLeft = -1, platformRight = -1,
characterRight = 1, platformLeft = 1,
characterTop = 2, platformBottom = 2,
characterBottom = -2, platformTop = -2,
none = 0,
tolerance = 10,
}
type PlatformColliderCallback = (hitOn: HitOn, exceeded: number, platform: any, character: any) => void;
type PlatformColliderExitCallback = (hittedOn: HitOn) => void;
/** LIMITATIONS:
* - If two platforms colliders are too close (<= characterAABB.width) you can get
* unexpected behaviours.
* - Only works with rectangle colliders
*/
export class PlatformCollider
{
/** Tolerancia en eje Y en px. Cuando hay dos bloques pisos seguidos, y el character va corriendo, si no hay
* un poco de tolerancia, el personaje cae al vacio. Ejemplo: Mario corriendo no cae al vacio en agujeros estrechos),
*/
yTolerance: number = 2; // luego setearlo a 1 o 2
private onHitCb: PlatformColliderCallback = null;
private onExitCb: PlatformColliderCallback = null;
private thisArgCb: any = null;
/** Count how many colliders has touched the player at same, ex: the player is over two platforms */
public touchingYCount: number = 0;
/** Count how many colliders has touched the player at same, ex: the player is next to two platforms */
public touchingXCount: number = 0;
/** Set the callbacks
*
* @param thisArg only pass `this`
* @param onHitCb (hitOn: HitOn, exceeded: number) => void;
* @param onExitCb (hittedOn: HitOn) => void;
*/
setCallbacks (thisArg: any, onHitCb: PlatformColliderCallback, onExitCb: PlatformColliderExitCallback) : void
{
this.onHitCb = onHitCb;
this.onExitCb = onExitCb;
this.thisArgCb = thisArg;
}
/** Difference in px to set the sprite right to the platform left @example this.node.x += this.platformCollider.toPlatformLeft (platf, chara);*/
toPlatformLeft (platf: any, chara: any) : number
{
return -1 * Math.abs (platf.world.aabb.xMin - chara.world.aabb.xMax);
}
/** Difference in px to set the sprite left to the platform right @example this.node.x += this.platformCollider.toPlatformRight (platf, chara);*/
toPlatformRight (platf: any, chara: any) : number
{
return Math.abs (platf.world.aabb.xMax - chara.world.aabb.xMin);
}
/** Call this from your onCollisionExit() */
public onCollisionExit (platf: any, chara: any) : void
{
// Si ha tocado esta plataforma
if (platf.touchingY)
{
this.onExitCb.call (this.thisArgCb, platf.touching);
this.touchingYCount--;
platf.touchingY = false;
platf.touching = HitOn.none;
}
// Si ha tocado esta plataforma
else if (platf.touchingX)
{
this.onExitCb.call (this.thisArgCb, platf.touching);
this.touchingXCount--;
platf.touchingX = false;
platf.touching = HitOn.none;
}
}
/** Call this from your onCollisionEnter() */
public onBoxColliderEnter (platf: any, chara: any) : void
{
let platfAabb = platf.world.aabb; let platfPreAabb = platf.world.preAabb.clone();
let charaAabb = chara.world.aabb; let charaPreAabb = chara.world.preAabb.clone();
// Sirve para ver, en el siguiente if, si hay una intersección, entonces quiere decir que hubo una colision en el eje Y
charaPreAabb.y = charaAabb.y;
platfPreAabb.y = platfAabb.y;
// Character in vertical collision.
if (cc.Intersection.rectRect (charaPreAabb, platfPreAabb))
{
this.onVerticalCollision (platf, chara);
return;
}
// Sirve para ver, en el siguiente if, si hay una intersección, entonces quiere decir que hubo una colision en el eje X
charaPreAabb.x = charaAabb.x;
platfPreAabb.x = platfAabb.x;
// Character in horizontal collision
if (cc.Intersection.rectRect (charaPreAabb, platfPreAabb))
{
// Este se llama acá para tener una tolerancia en el eje Y
if (charaAabb.yMin > platfAabb.yMax - Math.min (this.yTolerance, platf.world.aabb.height - 1))
{
this.onVerticalCollision (platf, chara, true);
return;
}
this.onHorizontalCollision (platf, chara);
return;
}
}
private onHorizontalCollision (platf: any, chara: any) : void
{
let exceeded: number;
let hitOn: HitOn;
// si la posiscion pre es menor a la current OR character.parte_derecha == platforma.parte_izquierda
if (chara.world.preAabb.x < chara.world.aabb.x || platf.world.aabb.xMin === chara.world.aabb.xMax)
{
exceeded = -1 * Math.abs (platf.world.aabb.xMin - chara.world.aabb.xMax);
hitOn = HitOn.platformLeft;
}
else
{
exceeded = Math.abs (platf.world.aabb.xMax - chara.world.aabb.xMin);
hitOn = HitOn.platformRight;
}
platf.touchingX = true;
this.touchingXCount++;
platf.touching = hitOn;
this.onHitCb.call (this.thisArgCb, hitOn, exceeded, platf, chara);
}
private onVerticalCollision (platf: any, chara: any, hitInTolerance: boolean = false) : void
{
let exceeded: number = 0;
let hitOn: HitOn = HitOn.none;
if (hitInTolerance)
{
hitOn = HitOn.tolerance;
exceeded = Math.abs (platf.world.aabb.yMax - chara.world.aabb.yMin);
}
// Si el pre está más abajo que el current OR character_top === platform_bottom
else if (chara.world.preAabb.y < chara.world.aabb.y || platf.world.aabb.yMin === chara.world.aabb.yMax)
{
exceeded = -1 * Math.abs (platf.world.aabb.yMin - chara.world.aabb.yMax);
hitOn = HitOn.platformBottom;
}
else // suelo de platform
{
exceeded = Math.abs (platf.world.aabb.yMax - chara.world.aabb.yMin);
hitOn = HitOn.platformTop;
}
platf.touchingY = true;
this.touchingYCount++;
platf.touching = hitOn;
this.onHitCb.call (this.thisArgCb, hitOn, exceeded, platf, chara);
}
}
Then, include this file in your player script, and use like this:
onLoad:
this.platformCollider = new PlatformCollider();
this.platformCollider.setCallbacks (this, this.onHitPlatform, this.onExitPlatform);
Calls from CC Collision callbacks:
onCollisionExit (other, self)
{
this.platformCollider.onCollisionExit (other, self);
}
// onCollisionStay (other, self) { }
onCollisionEnter (other, self)
{
this.platformCollider.onBoxColliderEnter (other, self);
}
Then, create two methods where you need to implement the logic
onHitPlatform (hitOn: HitOn, exceeded: number, platf: any, chara: any) : void;
onExitPlatform (hittedOn: HitOn);
For example, in your onHitPlatform():
// Player hits the bottom part of the platform, with his foots (bottom part of his collider)
if (hitOn === HitOn.platformTop && this.onGround === false)
{
this.setGrounded (true); // your function to set the player grounded, or simply this.grounded = true
this.jump.speed = 0; // Set the jump speed to zero, because the player must be in ground
this.node.y += exceeded; // move the player to the platform limits
}
I hope it works for you, but I still recommend using the physics engine (check Martin youtube channel for a tutorial: https://www.youtube.com/channel/UCe8Bfxd_3EKfvO24xbC3P3w)