Skip to content

Commit

Permalink
feat: add 2d character controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
Gugustinette committed Aug 23, 2024
1 parent 769f1e9 commit 0743d3e
Show file tree
Hide file tree
Showing 15 changed files with 400 additions and 62 deletions.
43 changes: 15 additions & 28 deletions apps/playground-2d/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import './style.css'
import { F2dShapes, FAttachedCamera, FCircle, FScene2d, FSprite, FSquare } from '@fibbojs/2d'
import { F2dShapes, FAttachedCamera, FCharacter2dKV, FCircle, FScene2d, FSprite, FSquare } from '@fibbojs/2d'
import { fDebug } from '@fibbojs/devtools'
import { FKeyboard } from '@fibbojs/event'
import MySquare from './classes/MySquare'

(async () => {
Expand Down Expand Up @@ -58,43 +57,31 @@ import MySquare from './classes/MySquare'
circle.initRigidBody()
scene.addComponent(circle)

// Create a sprite
const sprite = new FSprite(scene, '/fibbo/playground-2d/bunny.png')
sprite.onLoaded(() => {
sprite.setPosition(2, 3)
sprite.initRigidBody({
lockRotations: true,
})
sprite.setScaleWidth(0.5)
sprite.onCollisionWith(FSquare, () => {
console.log('Sprite collided with a square!')
})
sprite.onCollisionWith(circle, () => {
console.log('Sprite collided with the circle!')
})
})
scene.addComponent(sprite)

// Attach a camera to the cube
scene.camera = new FAttachedCamera(scene, {
target: sprite,
})

// Create a keyboard instance
const fKeyboard = new FKeyboard(scene)
// Detect inputs to move the cube
fKeyboard.on('ArrowUp', () => {
sprite.rigidBody?.applyImpulse({ x: 0, y: 0.1 }, true)
/**
* Create character
*/
const character = new FCharacter2dKV(scene)
character.onCollisionWith(FSquare, () => {
console.log('Sprite collided with a square!')
})
fKeyboard.on('ArrowDown', () => {
sprite.rigidBody?.applyImpulse({ x: 0, y: -0.1 }, true)
character.onCollisionWith(circle, () => {
console.log('Sprite collided with the circle!')
})
fKeyboard.on('ArrowLeft', () => {
sprite.rigidBody?.applyImpulse({ x: -0.1, y: 0 }, true)
})
fKeyboard.on('ArrowRight', () => {
sprite.rigidBody?.applyImpulse({ x: 0.1, y: 0 }, true)
})
fKeyboard.on(' ', () => {
sprite.rigidBody?.applyImpulse({ x: 0, y: 0.5 }, true)
scene.addComponent(character)

// Attach a camera to the character
scene.camera = new FAttachedCamera(scene, {
target: character,
})
})()
3 changes: 2 additions & 1 deletion packages/2d/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@fibbojs/core": "^0.0.1"
"@fibbojs/core": "^0.0.1",
"@fibbojs/event": "^0.0.1"
}
}
50 changes: 28 additions & 22 deletions packages/2d/src/FComponent2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ export interface FComponent2dOptions {
rotationDegree?: number
}

export interface FComponent2dOptions__initRigidBody {
position?: PIXI.PointData
scale?: PIXI.PointData
rotation?: number
shape?: F2dShapes
rigidBodyType?: RAPIER.RigidBodyType
lockTranslations?: boolean
lockRotations?: boolean
enabledTranslations?: {
enableX: boolean
enableY: boolean
}
}

export interface FComponent2dOptions__initCollider {
position?: PIXI.PointData
scale?: PIXI.PointData
rotation?: number
shape?: F2dShapes
}

/**
* @description The base class for all 2D components in FibboJS.
* @category Core
Expand Down Expand Up @@ -227,18 +248,7 @@ export abstract class FComponent2d extends FComponent {
* })
* ```
*/
initRigidBody(options?: {
position?: PIXI.PointData
scale?: PIXI.PointData
rotation?: number
shape?: F2dShapes
lockTranslations?: boolean
lockRotations?: boolean
enabledTranslations?: {
enableX: boolean
enableY: boolean
}
}): void {
initRigidBody(options?: FComponent2dOptions__initRigidBody): void {
// Apply default options
const DEFAULT_OPTIONS = {
position: new PIXI.Point(this.position.x, this.position.y),
Expand All @@ -258,10 +268,11 @@ export abstract class FComponent2d extends FComponent {
if (!this.scene.world)
throw new Error('FScene must have a world to create a rigid body')

// Create a dynamic rigid-body.
const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(options.position.x, options.position.y)
.setRotation(options.rotation)
// Create a rigid body description according to the type
const rigidBodyDesc = new RAPIER.RigidBodyDesc(options.rigidBodyType as RAPIER.RigidBodyType)
// Set translation and rotation for the rigid body
rigidBodyDesc.setTranslation(options.position.x, options.position.y)
rigidBodyDesc.setRotation(options.rotation)

this.rigidBody = this.scene.world.createRigidBody(rigidBodyDesc)

Expand Down Expand Up @@ -301,12 +312,7 @@ export abstract class FComponent2d extends FComponent {
* })
* ```
*/
initCollider(options?: {
position?: PIXI.PointData
scale?: PIXI.PointData
rotation?: number
shape?: F2dShapes
}): void {
initCollider(options?: FComponent2dOptions__initCollider): void {
// Apply default options
const DEFAULT_OPTIONS = {
position: new PIXI.Point(this.position.x, this.position.y),
Expand Down
2 changes: 1 addition & 1 deletion packages/2d/src/FScene2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface FScene2dOptions {
}

/**
* @description A scene which contains the models, the Three.js scene and the Rapier world.
* @description A scene which contains the models, the Pixi.js scene and the Rapier world.
* @category Core
* @example
* ```ts
Expand Down
114 changes: 114 additions & 0 deletions packages/2d/src/character/FCharacter2d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as PIXI from 'pixi.js'
import { FKeyboard } from '@fibbojs/event'
import type { FScene2d } from '../FScene2d'
import { F2dShapes } from '../types/F2dShapes'
import type { FComponent2dOptions, FComponent2dOptions__initCollider, FComponent2dOptions__initRigidBody } from '../FComponent2d'
import { FComponent2d } from '../FComponent2d'

export interface FCharacter2dOptions extends FComponent2dOptions {
/**
* The speed of the character.
*/
speed?: number
}

/**
* @description An abstract pre-defined character controller.
* @category Character
*/
export abstract class FCharacter2d extends FComponent2d {
/**
* The inputs that will be used to move the character.
*/
inputs: {
up: boolean
down: boolean
left: boolean
right: boolean
}

/**
* The speed of the character.
*/
speed: number

constructor(scene: FScene2d, options?: FCharacter2dOptions) {
super(scene, {
scale: { x: 0.5, y: 1 },
...options,
})

// Define default values
const DEFAULT_OPTIONS = {
speed: 1,
}
// Apply default options
options = { ...DEFAULT_OPTIONS, ...options }
// Validate options
if (!options.speed)
throw new Error('FCharacter2d requires speed option')

// Store speed
this.speed = options.speed

// Map of the movements (will be updated by the keyboard)
this.inputs = {
up: false,
down: false,
left: false,
right: false,
}

// Create a square
this.container = new PIXI.Graphics()
.rect(this.position.x, this.position.y, this.scale.x * 100, this.scale.y * 100)
.fill(new PIXI.FillGradient(0, 0, this.scale.x * 100, this.scale.y * 100).addColorStop(0, 0xFF00FF).addColorStop(1, 0xFFFF00))
// Set the pivot of the container to the center
this.container.pivot.set(this.container.width / 2, this.container.height / 2)

// Create a keyboard instance
const fKeyboard = new FKeyboard(scene)

// Key down
fKeyboard.onKeyDown('ArrowUp', () => {
this.inputs.up = true
})
fKeyboard.onKeyDown('ArrowDown', () => {
this.inputs.down = true
})
fKeyboard.onKeyDown('ArrowLeft', () => {
this.inputs.left = true
})
fKeyboard.onKeyDown('ArrowRight', () => {
this.inputs.right = true
})

// Key up
fKeyboard.onKeyUp('ArrowUp', () => {
this.inputs.up = false
})
fKeyboard.onKeyUp('ArrowDown', () => {
this.inputs.down = false
})
fKeyboard.onKeyUp('ArrowLeft', () => {
this.inputs.left = false
})
fKeyboard.onKeyUp('ArrowRight', () => {
this.inputs.right = false
})
}

initRigidBody(options?: FComponent2dOptions__initRigidBody): void {
super.initRigidBody({
shape: F2dShapes.SQUARE,
...options,
})
}

initCollider(options?: FComponent2dOptions__initCollider): void {
super.initCollider({
shape: F2dShapes.SQUARE,
...options,
})
}
}
54 changes: 54 additions & 0 deletions packages/2d/src/character/FCharacter2dDynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import RAPIER from '@dimforge/rapier2d'
import type { FScene2d } from '../FScene2d'
import type { FComponent2dOptions, FComponent2dOptions__initRigidBody } from '../FComponent2d'
import { FCharacter2d } from './FCharacter2d'

/**
* @description A pre-defined character controller based on Dynamic rigidbodies.
* @category Character
* @example
* ```ts
* import { FScene2d, FCharacter2dDynamic } from '@fibbojs/2d'
*
* const scene = new FScene2d()
*
* const capsule = new FCharacter2dDynamic(scene)
* scene.addComponent(capsule)
* ```
*/
export class FCharacter2dDynamic extends FCharacter2d {
constructor(scene: FScene2d, options?: FComponent2dOptions) {
super(scene, options)

/**
* Handle movements on each frame (only character movement, as gravity is handled by the dynamic rigid body)
*/
scene.onFrame(() => {
// Apply movement on the y axis
if (this.inputs.up) {
this.rigidBody?.applyImpulse({ x: 0, y: 0.15 * this.speed }, true)
}
else if (this.inputs.down) {
this.rigidBody?.applyImpulse({ x: 0, y: -0.15 * this.speed }, true)
}
// Apply movement on the x axis
if (this.inputs.left) {
this.rigidBody?.applyImpulse({ x: -0.15 * this.speed, y: 0 }, true)
}
else if (this.inputs.right) {
this.rigidBody?.applyImpulse({ x: 0.15 * this.speed, y: 0 }, true)
}
})

// Initialize the rigid body
this.initRigidBody()
}

initRigidBody(options?: FComponent2dOptions__initRigidBody): void {
super.initRigidBody({
rigidBodyType: RAPIER.RigidBodyType.Dynamic,
lockRotations: true,
...options,
})
}
}
65 changes: 65 additions & 0 deletions packages/2d/src/character/FCharacter2dKP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as THREE from 'three'
import RAPIER from '@dimforge/rapier2d'
import type { FScene2d } from '../FScene2d'
import type { FComponent2dOptions, FComponent2dOptions__initRigidBody } from '../FComponent2d'
import { FCharacter2dKinematic } from './FCharacter2dKinematic'

/**
* @description A pre-defined character controller based on Kinematic Position rigidbodies.
* @category Character
* @example
* ```ts
* import { FScene2d, FCharacter2dKP } from '@fibbojs/2d'
*
* const scene = new FScene2d()
*
* const capsule = new FCharacter2dKP(scene)
* scene.addComponent(capsule)
* ```
*/
export class FCharacter2dKP extends FCharacter2dKinematic {
constructor(scene: FScene2d, options?: FComponent2dOptions) {
super(scene, options)

/**
* Handle movements on each frame (gravity + character movement)
* For some reason, using the onFrame method will result in weird behavior with gravity
* (e.g. the character crossing the ground)
*/
scene.onFrame((delta) => {
const movementDirection = new RAPIER.Vector2(0, 0)
// Compute the movement direction
movementDirection.x = this.inputs.left ? -1 : this.inputs.right ? 1 : 0
// TODO : jump
// movementDirection.y = this.inputs.up ? 1 : this.inputs.down ? -1 : 0

// Create movement vector
const desiredMovement = {
x: movementDirection.x * delta * 8 * this.speed,
y: this.scene.world.gravity.y * delta,
}
// Compute the desired movement
this.characterController.computeColliderMovement(
this.collider as RAPIER.Collider,
desiredMovement,
)
// Get the corrected movement
const correctedMovement = this.characterController.computedMovement()
// Apply the movement to the rigid body
this.rigidBody?.setNextKinematicTranslation({
x: this.rigidBody.translation().x + correctedMovement.x * delta * this.speed * 64,
y: this.rigidBody.translation().y + correctedMovement.y * delta * this.speed * 64,
})
})

// Initialize the rigid body
this.initRigidBody()
}

initRigidBody(options?: FComponent2dOptions__initRigidBody): void {
super.initRigidBody({
rigidBodyType: RAPIER.RigidBodyType.KinematicPositionBased,
...options,
})
}
}
Loading

0 comments on commit 0743d3e

Please sign in to comment.