diff --git a/avatar-swap/.dclignore b/avatar-swap/.dclignore new file mode 100644 index 000000000..296236bd5 --- /dev/null +++ b/avatar-swap/.dclignore @@ -0,0 +1,14 @@ +.* +package.json +package-lock.json +yarn-lock.json +build.json +export +tsconfig.json +tslint.json +node_modules +*.ts +*.tsx +Dockerfile +dist +screenshots \ No newline at end of file diff --git a/avatar-swap/.eslintrc.json b/avatar-swap/.eslintrc.json new file mode 100644 index 000000000..91cac6e12 --- /dev/null +++ b/avatar-swap/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@dcl/eslint-config/sdk", + "parserOptions": { + "project": ["tsconfig.json"] + } +} diff --git a/avatar-swap/.gitignore b/avatar-swap/.gitignore new file mode 100644 index 000000000..2c2fba0f8 --- /dev/null +++ b/avatar-swap/.gitignore @@ -0,0 +1,7 @@ +package-lock.json +node_modules +bin +*.swp +*.*~ +export +.idea/ diff --git a/avatar-swap/README.md b/avatar-swap/README.md new file mode 100644 index 000000000..7b035e8b9 --- /dev/null +++ b/avatar-swap/README.md @@ -0,0 +1,40 @@ +# Avatar Swap +A scene that uses `AvatarModifier` component to swap out the default avatar for another character model. This way the player's character style can match that of the enviroment's. + +![](screenshots/avatar-swap.gif) + +This scene shows: +- How to add a 3D model +- How to play animations from a 3D model +- How to hide an avatar using `AvatarModifier` component +- How to attach an entity to the Player +- How to get the Player's entity `Transform` and its position + +## Instructions +Run over to the area covered in grass to automatically switch avatars. Use your mouse to look around and W A S D keys on your keyboard to move forward, left, backward and right respectively. + +## Try it out + +**Install the CLI** + +Download and install the Decentraland CLI by running the following command inside this scene root directory: + +```bash +npm install @dcl/sdk@next +``` + +**Previewing the scene** + +Inside this scene root directory run: + +``` +dcl start +``` + +## Copyright info + +This scene is protected with a standard Apache 2 licence. See the terms and conditions in the [LICENSE](/LICENSE) file. + +## Acknowledgements + +Model and animations from https://www.mixamo.com/ diff --git a/avatar-swap/models/arissa.glb b/avatar-swap/models/arissa.glb new file mode 100644 index 000000000..cd620db34 Binary files /dev/null and b/avatar-swap/models/arissa.glb differ diff --git a/avatar-swap/models/baseGrass.glb b/avatar-swap/models/baseGrass.glb new file mode 100644 index 000000000..baa9f03d5 Binary files /dev/null and b/avatar-swap/models/baseGrass.glb differ diff --git a/avatar-swap/package.json b/avatar-swap/package.json new file mode 100644 index 000000000..b837aeace --- /dev/null +++ b/avatar-swap/package.json @@ -0,0 +1,16 @@ +{ + "name": "avatar-swap", + "version": "1.0.0", + "description": "Avatar Swap", + "scripts": { + "start": "dcl start", + "build": "build-ecs", + "watch": "build-ecs --watch", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix" + }, + "devDependencies": { + "@dcl/eslint-config": "^1.0.1", + "@dcl/sdk": "next" + } +} diff --git a/avatar-swap/scene.json b/avatar-swap/scene.json new file mode 100644 index 000000000..3193d426d --- /dev/null +++ b/avatar-swap/scene.json @@ -0,0 +1,37 @@ +{ + "ecs7": true, + "display": { + "title": "avatar-swap", + "favicon": "favicon_asset" + }, + "contact": { + "name": "decentraland", + "email": "" + }, + "owner": "", + "scene": { + "parcels": [ + "23,23" + ], + "base": "23,23" + }, + "requiredPermissions": [], + "main": "bin/game.js", + "tags": [], + "spawnPoints": [ + { + "name": "spawn1", + "default": true, + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "cameraTarget": { + "x": 8, + "y": 1, + "z": 8 + } + } + ] +} \ No newline at end of file diff --git a/avatar-swap/screenshots/avatar-swap.gif b/avatar-swap/screenshots/avatar-swap.gif new file mode 100644 index 000000000..b4f29afdc Binary files /dev/null and b/avatar-swap/screenshots/avatar-swap.gif differ diff --git a/avatar-swap/src/index.ts b/avatar-swap/src/index.ts new file mode 100644 index 000000000..85fe34ef5 --- /dev/null +++ b/avatar-swap/src/index.ts @@ -0,0 +1,28 @@ +export * from '@dcl/sdk' +import { engine, GltfContainer, Transform } from '@dcl/sdk/ecs' +import { Vector3 } from '@dcl/sdk/math' +import { attachEntityToPlayer } from "./modules/utils"; +import { createArissaCharacter } from "./modules/arissa"; +import { createAvatarSwappingArea, avatarSwappingSystem } from "./modules/avatarSwappingArea"; + +function setup() { + // Instantiate ground model + const groundEntity = engine.addEntity() + GltfContainer.create(groundEntity, { + src: "models/baseGrass.glb" + }) + + // Instantiate 'Arissa' character animated model + const arissaCharaEntity = createArissaCharacter() + attachEntityToPlayer(Transform.get(arissaCharaEntity).parent) + + // Set avatar modifier area to swap player avatar + createAvatarSwappingArea(Vector3.create(8, 2, 10.5), Vector3.create(16, 4, 11), arissaCharaEntity) + + // Register avatar swapping system + engine.addSystem(avatarSwappingSystem) +} + +setup() + + diff --git a/avatar-swap/src/modules/arissa.ts b/avatar-swap/src/modules/arissa.ts new file mode 100644 index 000000000..17565aa02 --- /dev/null +++ b/avatar-swap/src/modules/arissa.ts @@ -0,0 +1,38 @@ +import { + engine, + Entity, + GltfContainer, + Transform, + Animator +} from '@dcl/sdk/ecs' +import { Vector3 } from "@dcl/sdk/math"; + +export function createArissaCharacter() : Entity { + const parentEntity = engine.addEntity() + const entity = engine.addEntity() + + GltfContainer.create(entity, { + src: "models/arissa.glb" + }) + Transform.create(entity, { + position: Vector3.create(0, 1.75, 0), + scale: Vector3.create(0, 0, 0), + parent: parentEntity + }) + Animator.create(entity, { + states: [ + { + name: "Running", + clip: "Running", + loop: true + }, + { + name: "Idle", + clip: "Idle", + loop: true + } + ] + }) + + return entity +} \ No newline at end of file diff --git a/avatar-swap/src/modules/avatarSwappingArea.ts b/avatar-swap/src/modules/avatarSwappingArea.ts new file mode 100644 index 000000000..ed2c6bcb0 --- /dev/null +++ b/avatar-swap/src/modules/avatarSwappingArea.ts @@ -0,0 +1,76 @@ +import { + engine, + Entity, + Transform, + Animator, + AvatarModifierArea, + AvatarModifierType +} from '@dcl/sdk/ecs' +import { Vector3 } from '@dcl/sdk/math' + +let areaCenter: Vector3 +let areaSize: Vector3 +let areaMinPosition: Vector3 +let areaMaxPosition: Vector3 + +function setupAreaData(center: Vector3, size: Vector3) { + areaCenter = center + areaSize = size + + const halfSize = Vector3.scale(size, 0.5) + areaMinPosition = Vector3.create( + areaCenter.x - halfSize.x, + areaCenter.y - halfSize.y, + areaCenter.z - halfSize.z + ) + areaMaxPosition = Vector3.create( + areaCenter.x + halfSize.x, + areaCenter.y + halfSize.y, + areaCenter.z + halfSize.z + ) +} + +function isPositionInsideArea(targetPosition: Vector3): boolean { + return targetPosition.x > areaMinPosition.x + && targetPosition.y > areaMinPosition.y + && targetPosition.z > areaMinPosition.z + && targetPosition.x < areaMaxPosition.x + && targetPosition.y < areaMaxPosition.y + && targetPosition.z < areaMaxPosition.z +} + +let otherAvatarEntity: Entity +export function createAvatarSwappingArea(center: Vector3, size: Vector3, avatarEntity: Entity) { + setupAreaData(center, size) + otherAvatarEntity = avatarEntity + const avatarHiderAreaEntity = engine.addEntity() + AvatarModifierArea.create(avatarHiderAreaEntity, { + area: areaSize, + modifiers: [AvatarModifierType.AMT_HIDE_AVATARS], + excludeIds: [] + }) + Transform.create(avatarHiderAreaEntity, { + position: areaCenter + }) +} + +let lastPlayerPos: Vector3 | undefined = undefined +export function avatarSwappingSystem (dt: number) { + if (!Transform.has(engine.PlayerEntity)) return + + const playerPos = Transform.get(engine.PlayerEntity).position + const moved = playerPos != lastPlayerPos + + Animator.getClip(otherAvatarEntity, "Idle").playing = !moved + Animator.getClip(otherAvatarEntity, "Running").playing = moved + + if (!moved) return + + const playerIsInsideHidingArea = isPositionInsideArea(playerPos) + const otherAvatarTransform = Transform.getMutable(otherAvatarEntity) + otherAvatarTransform.scale.x = playerIsInsideHidingArea ? 1.1 : 0 + otherAvatarTransform.scale.y = playerIsInsideHidingArea ? 1.1 : 0 + otherAvatarTransform.scale.z = playerIsInsideHidingArea ? 1.1 : 0 + + lastPlayerPos = playerPos +} \ No newline at end of file diff --git a/avatar-swap/src/modules/utils.ts b/avatar-swap/src/modules/utils.ts new file mode 100644 index 000000000..6a7fe1252 --- /dev/null +++ b/avatar-swap/src/modules/utils.ts @@ -0,0 +1,13 @@ +import { AvatarAnchorPointType, AvatarAttach, Entity } from "@dcl/sdk/ecs"; +import { getUserData } from "~system/UserIdentity" + +export async function attachEntityToPlayer (entity: Entity){ + let userData = await getUserData({}) + if(!userData.data) return + console.log(`userId: ${userData.data.userId}`) + + AvatarAttach.create(entity, { + anchorPointId: AvatarAnchorPointType.AAPT_POSITION, + avatarId: userData.data.userId + }) +} \ No newline at end of file diff --git a/avatar-swap/tsconfig.json b/avatar-swap/tsconfig.json new file mode 100644 index 000000000..91f9d6d06 --- /dev/null +++ b/avatar-swap/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": {}, + "include": ["src/**/*.ts", "@dcl/ecs"], + "extends": "@dcl/sdk/types/tsconfig.ecs7.strict.json" +}