Skip to content

Commit db9eda6

Browse files
committed
Initial commit
0 parents  commit db9eda6

147 files changed

Lines changed: 16842 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.png filter=lfs diff=lfs merge=lfs -text
2+
*.jpg filter=lfs diff=lfs merge=lfs -text
3+
*.jpeg filter=lfs diff=lfs merge=lfs -text
4+
src/assets/** filter=lfs diff=lfs merge=lfs -text
5+
external/** filter=lfs diff=lfs merge=lfs -text

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
tmp
4+
.DS_Store

LICENSE

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Copyright 2026 8th Wall, Inc.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4+
associated documentation files (the "Software"), to deal in the Software without restriction,
5+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
6+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7+
furnished to do so, subject to the following conditions:
8+
9+
The above copyright notice and this permission notice shall be included in all copies or substantial
10+
portions of the Software.
11+
12+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
15+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Physics Playground
2+
3+
This example studio project demonstrates how to build multiple player controllers, switch between them dynamically, and integrate physics-driven gameplay with smooth camera tracking.
4+
5+
![](./src/assets/preview.gif)
6+
7+
## Usage
8+
9+
1. [Install the Desktop App](https://8thwall.org/downloads)
10+
2. On this repository, click Code > Download zip
11+
3. Unzip the folder to the location you'd like to work in
12+
4. In the desktop app, click "Open" and select the folder
13+
5. Recommended: Track your files using [git](https://git-scm.com/about) to avoid losing progress
14+
15+
## Questions?
16+
17+
Please raise any questions on [Github Discussions](https://github.com/orgs/8thwall/discussions) or join the [Discord](https://8th.io/discord) to connect with the community.
18+
19+
## Overview
20+
21+
Players can swap between two vehicles:
22+
23+
- Beach Ball - a rolling ball with classic movement and jump mechanics.
24+
- Rocket - a thrust-based vehicle with tilt, stabilization, and directional boosters.
25+
26+
## Controls
27+
28+
- WASD / Arrow Keys - Movement
29+
- Space - Jump (Ball) / Thrust (Rocket)
30+
- Shift - Stabilize (Rocket)
31+
- UI Buttons - Switch between Beach Ball and Rocket
32+
33+
## Components
34+
35+
### vehicle-swap
36+
37+
The vehicleSwap component manages switching between the ball and rocket controllers. It updates active states, swaps models, and dispatches global events so systems stay in sync.
38+
39+
Schema:
40+
41+
- rocketButton: UI element for selecting the rocket
42+
- beachBallButton: UI element for selecting the ball
43+
- rocketModel: Asset path for rocket model
44+
- beachBallModel: Asset path for beach ball model
45+
46+
State Machine:
47+
48+
- ballActive: Ball model is shown, ball controller enabled
49+
- rocketActive: Rocket model is shown, rocket controller enabled
50+
51+
### ballController
52+
53+
The ballController enables rolling physics and jump mechanics. It supports both direct movement and torque-based control for variety.
54+
55+
Schema:
56+
57+
- force: Movement force applied per input
58+
- jumpForce: Impulse force applied when jumping
59+
- jumpControlFactor: Air control multiplier
60+
- torqueForce: Additional torque applied when modifier is held
61+
62+
Data:
63+
64+
- isGrounded: True if in contact with surface
65+
- collisionCount: Number of active collisions
66+
- velocityX / velocityZ: Tracked movement speed
67+
- isActive: Whether the ball controller is currently active
68+
69+
The rocketController simulates thrust-based flight and tilt controls with optional stabilization. Particle systems toggle with thrust input for visual feedback.
70+
71+
Schema:
72+
73+
- torqueStrength: Rotation force applied by tilt input
74+
- mainThrustForce: Upward lift from main booster
75+
- lateralThrustForce: Side thrust for precision movement
76+
- rotationDamping: Dampens angular velocity for stability
77+
- maxTiltAngle: Tilt limit in radians
78+
79+
Data:
80+
81+
- isGrounded: True if rocket is touching ground
82+
- collisionCount: Active collisions count
83+
- velocityX / velocityZ: Horizontal speed tracking
84+
- isThrusting: Whether thrust is active
85+
- isActive: Whether rocket controller is active
86+
- quaternion (x,y,z,w): Stores current rotation
87+
88+
## Input Manager
89+
90+
The Input Manager maps multiple input methods into unified actions:
91+
92+
- forward — W / Up arrow / gamepad stick up
93+
- backward — S / Down arrow / gamepad stick down
94+
- left — A / Left arrow / gamepad stick left
95+
- right — D / Right arrow / gamepad stick right
96+
- jump — Space bar
97+
- modify — Shift (used for rocket stabilization and ball torque mode)
98+
99+
## Asset Attribution
100+
101+
[Pirate Kit](https://quaternius.com/packs/piratekit.html) By Quaternius,
102+
[Pirate Kit](https://kenney.nl/assets/pirate-kit) By Kenney,
103+
[Chest Game Asset Animated](https://sketchfab.com/3d-models/chest-game-asset-animated-ca26393a972c49ba80f7de01f04b9edb) By Johnny Rogue

config/LICENSE

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Copyright 2025 Niantic Spatial, Inc.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4+
associated documentation files (the "Software"), to deal in the Software without restriction,
5+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
6+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7+
furnished to do so, subject to the following conditions:
8+
9+
The above copyright notice and this permission notice shall be included in all copies or substantial
10+
portions of the Software.
11+
12+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
15+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

config/asset-loader.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const path = require('path')
2+
3+
const toUnixPath = process.platform === 'win32'
4+
? p => p.replace(/\\/g, '/').replace(/\/+/g, '/')
5+
: p => p
6+
7+
const makeExport = srcPath => `module.exports = ${JSON.stringify(toUnixPath(srcPath))};`
8+
9+
// Export the asset path as asset content
10+
function assetLoader() {
11+
const srcPath = path.relative(this.rootContext, this.resourcePath)
12+
13+
// Export the relative path for the asset file
14+
return makeExport(srcPath)
15+
}
16+
17+
module.exports = assetLoader

config/entry-plugin.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
const path = require('path')
2+
const VirtualModulesPlugin = require('webpack-virtual-modules')
3+
4+
const isCodeFile = file => file.endsWith('.ts') || file.endsWith('.js')
5+
const IGNORED_FOLDERS = ['assets', '.dependencies']
6+
7+
const SCENE_INIT_CONTENT = `
8+
import scene from './.expanse.json'
9+
10+
delete scene.history
11+
delete scene.historyVersion
12+
13+
window.ecs.application.init(scene)
14+
15+
if (module.hot) {
16+
const isInline = window.location.href.includes('liveSyncMode=inline')
17+
18+
const handler = isInline
19+
? () => { }
20+
: async () => {
21+
const updatedScene = (await import('./.expanse.json')).default
22+
23+
delete updatedScene.history
24+
delete updatedScene.historyVersion
25+
26+
window.ecs.application.getScene().updateBaseObjects(updatedScene.objects)
27+
window.ecs.application.getScene().updateDebug(updatedScene)
28+
}
29+
30+
module.hot.accept('./.expanse.json', handler)
31+
}`
32+
33+
const createVirtualEntryPlugin = (options = {}) => {
34+
const {srcDir} = options
35+
const virtualModules = new VirtualModulesPlugin()
36+
const importedFiles = new Set()
37+
const ignoredFoldersPaths = IGNORED_FOLDERS.map(folder => path.join(srcDir, folder))
38+
39+
let hasInit = false
40+
let watcher = null
41+
42+
const removeImports = (files) => {
43+
files.forEach(file => importedFiles.delete(file))
44+
}
45+
46+
const updateVirtualModules = () => {
47+
const appFiles = Array.from(importedFiles).filter(
48+
file => file === path.join(srcDir, 'app.js') || file === path.join(srcDir, 'app.ts')
49+
)
50+
const otherFiles = Array.from(importedFiles).filter(
51+
file => !appFiles.includes(file)
52+
)
53+
54+
const imports = [...appFiles, ...otherFiles]
55+
.map((file) => {
56+
const relativePath = path.relative(srcDir, file)
57+
return `import ${JSON.stringify(`./${relativePath.replace(/\\/g, '/')}`)}`
58+
})
59+
.join('\n')
60+
61+
const content = `${imports}\n${SCENE_INIT_CONTENT}`
62+
63+
virtualModules.writeModule(path.join(srcDir, 'entry.js'), content)
64+
}
65+
66+
const initFileWatcher = async () => {
67+
if (watcher) {
68+
return
69+
}
70+
71+
try {
72+
const chokidar = await import('chokidar')
73+
watcher = chokidar.default.watch(srcDir, {
74+
ignored: filePath => ignoredFoldersPaths.some(folder => filePath.startsWith(folder)),
75+
persistent: true,
76+
})
77+
78+
watcher.on('add', (filePath) => {
79+
if (isCodeFile(filePath)) {
80+
importedFiles.add(filePath)
81+
updateVirtualModules()
82+
}
83+
})
84+
} catch (err) {
85+
// chokidar not available, file watching disabled
86+
}
87+
}
88+
89+
const closeFileWatcher = () => {
90+
if (watcher) {
91+
watcher.close()
92+
watcher = null
93+
}
94+
}
95+
96+
const getCodeFiles = (fs, dir) => {
97+
const files = fs.readdirSync(dir)
98+
files.forEach((file) => {
99+
const fullPath = path.join(dir, file)
100+
try {
101+
if (fs.lstatSync(fullPath).isDirectory()) {
102+
if (!IGNORED_FOLDERS.includes(file)) {
103+
getCodeFiles(fs, fullPath)
104+
}
105+
} else if (isCodeFile(fullPath)) {
106+
importedFiles.add(fullPath)
107+
}
108+
} catch (err) {
109+
// ignore virtual files
110+
}
111+
})
112+
}
113+
114+
return {
115+
apply: (compiler) => {
116+
compiler.hooks.watchRun.tapAsync('VirtualEntryPlugin', async (compilation, callback) => {
117+
const {removedFiles} = compilation
118+
119+
if (!hasInit) {
120+
hasInit = true
121+
updateVirtualModules()
122+
await initFileWatcher()
123+
}
124+
125+
if (removedFiles) {
126+
const removedCodeFiles = Array.from(removedFiles).filter(isCodeFile)
127+
if (removedCodeFiles.length > 0) {
128+
removeImports(removedCodeFiles)
129+
updateVirtualModules()
130+
}
131+
}
132+
133+
callback()
134+
})
135+
136+
compiler.hooks.shutdown.tap('VirtualEntryPlugin', () => {
137+
closeFileWatcher()
138+
})
139+
140+
compiler.hooks.beforeCompile.tapAsync('VirtualEntryPlugin', (_, callback) => {
141+
if (!compiler.watching && !hasInit) {
142+
getCodeFiles(compiler.inputFileSystem, srcDir)
143+
updateVirtualModules()
144+
hasInit = true
145+
}
146+
callback()
147+
})
148+
149+
virtualModules.apply(compiler)
150+
},
151+
}
152+
}
153+
154+
module.exports = createVirtualEntryPlugin

config/types/assets.d.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
declare module './assets/*' {
2+
const content: string
3+
export default content
4+
}
5+
6+
declare module '../assets/*' {
7+
const content: string
8+
export default content
9+
}
10+
11+
declare module '../../assets/*' {
12+
const content: string
13+
export default content
14+
}
15+
16+
declare module '../../../assets/*' {
17+
const content: string
18+
export default content
19+
}
20+
21+
declare module '../../../../assets/*' {
22+
const content: string
23+
export default content
24+
}
25+
26+
declare module '../../../../../assets/*' {
27+
const content: string
28+
export default content
29+
}
30+
31+
declare module '../../../../../../assets/*' {
32+
const content: string
33+
export default content
34+
}
35+
36+
declare module '../../../../../../../assets/*' {
37+
const content: string
38+
export default content
39+
}

config/types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './assets.d'

0 commit comments

Comments
 (0)