A-frame and ThreeJs intro
If you haven't tried it, A-frame is an amazing framework for creating 3D games and experiences. It is built on top of ThreeJS so you have lots of powerful tools at your disposal.
Threejs offers so much functionality for rendering and 3d scene control but doesnt include any game engine like structure. With a little work you can put together some of your own code to facilitate game state, levels, scoring ect.
The more I worked with threejs the more I wanted a way to write out my levels and objects in a tree like heirarchy similar to unity's heirarchy view. A tree like view showing all my objects and properties would be more ideal then declaring new Object3D() and then scene.add() ing them. I experimented with a tsx like syntax for doing this but ended up stumbling on aframe which gives you that exact feature and a lot more!
My aframe setup
I like to use typescript and webpack to build my projects source code. Bellow is my folder structure.
As for code see bellow:
src/index.ts
import "./aframe/components";
import "./aframe/entities";
Haha yeah that doesnt explain much now does it. In the components folder I have another index.ts file that looks mostly the same. It imports my custom components.
src/aframe/components/index.ts
import "./loadDefaultScene";
import "./prefabLoader";
import "./playerController";
import "./health"
import "./spring-control"
require('aframe-look-at-component');
This file ensures all my custom components get included in the build and and external component libraries are also included. The entities folder is the same concept.
Using lit-html with a-frame
This is sort of the secret sauce of my setup. Using lit-html to write up prefabs in dom syntax and then reuse them when ever I like. I also use this for level loading! All that is required is a few a-frame components and entities.
Prefabs:
src/aframe/entities/prefab.ts
AFRAME.registerPrimitive( "a-prefab",{
defaultComponents: {
"prefab-loader": {}
},
mappings: {
prefab: 'prefab-loader.prefab'
}
});
src/aframe/components/prefabLoader
import {Config} from "../../utils/config";
import {render, html, } from 'lit-html';
AFRAME.registerComponent('prefab-loader', {
schema: {
prefab:{}
},
init: function () {
render( Config.prefabs[this.data.prefab], this.el);
}
});
src/utils/config.ts
import { TemplateResult } from "lit-html";
import { scene1 } from "../scenes/scene1";
class GameConfig{
defaultScene = scene1;
prefabs: [] = [];
Prefab = function(template: TemplateResult){
let indexOf = this.prefabs.indexOf(template);
if( indexOf == -1){
let index = this.prefabs.push(template) - 1;
return index;
}else{
return indexOf;
}
}
}
export const Config = new GameConfig();
What these components give us is a <a-prefab /> entity where we can pass in our prefab to be loaded. Here is an example player prefab:
src/prefabs/car.ts
import {html} from 'lit-html';
let tires = [
"-1 0 1",
" 1 0 1",
"-1 0 -1",
" 1 0 -1",
]
export const car = html`
<a-box wasd-controls color="tomato" depth="4" height="3" width="2">
${tires.map( (i) => html`
<a-cylinder position="${i}" rotation=" 0 0 90"color="black" height=".5" radius=".7"></a-cylinder>
`)}
</a-box>
`;
src/prefabs/cameraRig.ts
import {html} from 'lit-html';
export const cameraRig = html`<a-entity
light="type:directional;
color:#fdffeb;
intensity:1.6;
castShadow:true;
shadowCameraLeft: -15;
shadowCameraBottom: -15;
shadowCameraRight: 15;
shadowCameraTop: 15;
shadowMapHeight: 2048;
shadowMapWidth: 2048;
target:#directionaltarget"
position="5 10 5"
></a-entity>
<a-entity id="directionaltarget" position="0 0 0"></a-entity>
<a-entity
id="rig"
position="0 0 0"
rotation="-50 0 0"
>
<a-camera
id="camera"
wasd-controls-enabled="false"
look-controls-enabled="false"
fov="14"
position="0 0 46"
></a-camera>
</a-entity>`;
Here is the prefab in use in a similar style template for a scene.
src/scenes/scene1.ts
import {html} from 'lit-html';
import { Config } from '../utils/config';
import { car } from '../prefabs/car';
import { cameraRig } from '../prefabs/cameraRig';
export const scene1 = () => html`
<!-- Ground -->
<a-box shadow position="0 -0.5 0" scale="20 1 20" color="#7b8f80" width="3" height="1" depth="1" ammo-body="type: static; collisionFilterGroup: 1; collisionFilterMask: 1;" ammo-shape="type: box"></a-box>
<!-- Sky and ambient light -->
<a-sky color="#94f6ff"></a-sky>
<a-entity light="type: hemisphere; color:#154354; groundColor:#7b8f80;"></a-entity>
<!-- camera and car prefabs -->
<a-prefab shadow position="0 0.7 -7" prefab="${Config.Prefab(car)}" ></a-prefab>
<a-prefab position="0 0 -7" prefab="${Config.Prefab(cameraRig)}" ></a-prefab>
`;
Scene loading:
Like our prefab loader we can create a component that auto loads our initial scene which can be extended to also manages scene swapping.
src/aframe/components/loadDefaultScene.ts
import {Config} from "../../utils/config";
import {render} from 'lit-html';
AFRAME.registerComponent('load-default-scene', {
init: function () {
var sceneEl = this.el;
var sceneContainer = document.createElement("a-entity");
render(Config.defaultScene(), sceneContainer);
sceneContainer.childNodes.forEach(element => {
sceneEl.appendChild(element)
});
}
});
Now to load our scene we just add this component to our <a-scene>
src/dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<script src="main.js"></script>
</script>
</head>
<body>
<a-scene load-default-scene >
</a-scene>
</body>
</html>
Comments
☕ Buy me a coffee