Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow networked entities to become children of a non-networked parent #401

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ <h1>Networked-Aframe Examples</h1>
<a href="basic-events.html">Basic with events</a>
<span>Example of listening to and logging NAF events</span>
</div>
<div>
<a href="non-networked-paraent.html">Non Networked Parent</a>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be "non-networked-parent.html"

<span>Demonstrates how to attach networked entities as childeren of non-networked parents</span>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be "entities as children"

</div>
<div>
<a href="ownership-transfer.html">Ownership Transfer</a>
<span>Demonstrates transfering ownership of entities</span>
Expand Down
188 changes: 188 additions & 0 deletions examples/non-networked-paraent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Basic Example — Networked-Aframe</title>
<meta name="description" content="Basic Example — Networked-Aframe" />

<script src="https://aframe.io/releases/1.4.1/aframe.min.js"></script>

<!-- NAF basic requirements -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.5.0/socket.io.slim.js"></script>
<script src="/easyrtc/easyrtc.js"></script>
<script src="/dist/networked-aframe.js"></script>

<!-- used for flying in this demo -->
<script src="https://cdn.jsdelivr.net/gh/n5ro/aframe-extras@14411ea/dist/aframe-extras.controls.min.js"></script>

<!-- used for the pretty environment -->
<script src="https://unpkg.com/aframe-environment-component@1.3.2/dist/aframe-environment-component.min.js"></script>

<!-- used to prevent players from spawning on top of each other so much -->
<script src="/js/spawn-in-circle.component.js"></script>

<script>
// Called by Networked-Aframe when connected to server (optional)
// (this api will change in future versions)
function onConnect() {
console.log('onConnect', new Date());
}

// Note the way we're establishing the NAF schema here; this is a bit awkward
// because of a recent bug found in the original handling. This mitigates that bug for now,
// until a refactor in the future that should fix the issue more cleanly.
// see issue https://github.com/networked-aframe/networked-aframe/issues/267

// This one is necessary, because tracking the .head child component's material's color
// won't happen unless we tell NAF to keep it in sync, like here.
NAF.schemas.getComponentsOriginal = NAF.schemas.getComponents;
NAF.schemas.getComponents = (template) => {
if (!NAF.schemas.hasTemplate('#avatar-template')) {
NAF.schemas.add({
template: '#avatar-template',
components: [
// position and rotation are added by default if we don't include a template, but since
// we also want to sync the color, we need to specify a custom template; if we didn't
// include position and rotation in this custom template, they'd not be synced.
'position',
'rotation',

// this is how we sync a particular property of a particular component for a particular
// child element of template instances.
{
selector: '.head',
component: 'material',
property: 'color' // property is optional; if excluded, syncs everything in the component schema
}
]
});
}
const components = NAF.schemas.getComponentsOriginal(template);
return components;
};

// Move the platform up
function movePlatformUp() {
platform.setAttribute('position', { y: platform.getAttribute('position').y + 5 });
}

// Move the platform down
function movePlatformDown() {
platform.setAttribute('position', { y: platform.getAttribute('position').y - 5 });
}

// Move the platform back to the starting height
function restPlatformHeight() {
platform.setAttribute('position', { y: 10 });
}
</script>
<script src="https://unpkg.com/aframe-randomizer-components@^3.0.1/dist/aframe-randomizer-components.min.js"></script>
<style>
button {
font-size: 24px;
padding: 12px 24px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
position: relative;
z-index: 10;
}

#resetButton {
background-color: rgb(126, 102, 102);
}

button:hover {
background-color: #0b7dda;
}
</style>
</head>
<body>
<button id="upButton" onclick="movePlatformUp()" class="button">Move Up</button>
<button id="downButton" onclick="movePlatformDown()" class="button">Move Down</button>
<button id="resetButton" onclick="restPlatformHeight()" class="button resetButton">Reset</button>

<a-scene
networked-scene="
room: basic;
debug: true;
adapter: wseasyrtc;
"
>
<a-assets>
<!-- Templates -->
<!-- Camera Rig / Player -->
<template id="rig-template">
<a-entity></a-entity>
</template>

<!-- Head / Avatar -->
<!-- a few spheres make a head + eyes + pupils -->
<template id="avatar-template">
<a-entity class="avatar">
<!-- notice this child sphere, with class .head, has the random-color component; this modifies the material component's color property -->
<a-sphere class="head" scale="0.2 0.22 0.2" random-color></a-sphere>
<a-entity class="face" position="0 0.05 0">
<a-sphere class="eye" color="white" position="0.06 0.05 -0.16" scale="0.04 0.04 0.04">
<a-sphere class="pupil" color="black" position="0 0 -1" scale="0.2 0.2 0.2"></a-sphere>
</a-sphere>
<a-sphere class="eye" color="white" position="-0.06 0.05 -0.16" scale="0.04 0.04 0.04">
<a-sphere class="pupil" color="black" position="0 0 -1" scale="0.2 0.2 0.2"></a-sphere>
</a-sphere>
</a-entity>
</a-entity>
</template>
<!-- /Templates -->
</a-assets>

<a-entity environment="preset:starry;groundColor:#000000;"></a-entity>
<a-entity light="type:ambient;intensity:0.5"></a-entity>

<!-- Networked entities will be added as children of the platform -->
<a-box id="platform" color="tomato" depth="10" height=".5" width="10" position="0 10 0">
<!-- Here we declare only the local user's avatar, which we then broadcast to other users -->
<!-- The 'spawn-in-circle' component will set the position and rotation of #rig;
because this entity also has the networked component, and position and rotation are tracked by default,
the changes made by spawn-in-circle will be kept in sync with other networked users.
Also note that by adding the networked component with a template reference, we generate that full template,
including all applicable child elements. However, because we don't need to see our own avatar, we use the
`attachTemplateToLocal:false` option. This makes our local copies invisible on our machine, but visible on everyone else's.
-->
<a-entity
id="rig"
movement-controls="fly:true;"
spawn-in-circle="radius:3"
networked="template:#rig-template; allowNonNetworkedParent:true;"
>
<!-- We add allowNonNetworkedParent because we want networked entities to end up as a child of platform entity not the root a-scene.
-->
Comment on lines +159 to +160

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that we've added this documentation as comments. I would recommend also adding additional information visible in the UI of this example explaining what this example is and how to use it without needing to inspect the source code. See the "Persistent spheres" example which includes the header at the top as an example.

<!-- Here we add the camera. Adding the camera within a 'rig' is standard practice.
We set the camera to head height for e.g. computer users, but otherwise never touch it again; if the user enters VR,
its rotation and position will be updated by the headset in VR. If we need to touch the user's position
or rotation, we always do that by adjusting the rig parent of the active camera. By making that rig--and the
active camera appended to it--both networked, we ensure all player movement is kept in sync.
-->
<a-entity
id="player"
camera
position="0 1.6 0"
look-controls
networked="template:#avatar-template;"
visible="false"
>
</a-entity>
</a-entity>
</a-box>
</a-scene>

<script>
// Called by Networked-Aframe when connected to server
// Optional to use; this API will change in the future
function onConnect() {
console.log('onConnect', new Date());
}
</script>
</body>
</html>
15 changes: 14 additions & 1 deletion src/NetworkEntities.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,17 @@ class NetworkEntities {
var parent = entityData.parent;
var networkId = entityData.networkId;

var parentNotCreatedYet = parent && !this.hasEntity(parent);
var parentNotCreatedYet
var cssIdentifierRegex = /^-?(?=[a-zA-Z\xA0-\uFFFF])([a-zA-Z0-9_\u00A0-\uFFFF]*)(-[a-zA-Z0-9_\u00A0-\uFFFF]+)*$/;
var isValidCssClass = cssIdentifierRegex.test(parent)
if (!parent) {
parentNotCreatedYet = false
} else if (isValidCssClass && document.querySelector(`#${parent}`)) {
parentNotCreatedYet = false
} else if (!this.hasEntity(parent)) {
parentNotCreatedYet = true
}

if (parentNotCreatedYet) {
this.childCache.addChild(parent, entityData);
} else {
Expand Down Expand Up @@ -122,8 +132,11 @@ class NetworkEntities {
}

addEntityToPage(entity, parentId) {
var nonNetworkedParent = document.querySelector(`#${parentId}`)
if (this.hasEntity(parentId)) {
this.addEntityToParent(entity, parentId);
} else if (nonNetworkedParent) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend setting line 135 to just var nonNetworkedParent; and then the assignment for nonNetworkedParent could instead happen on this line as:

else if ((nonNetworkedParent = document.querySelector(#${parentId})))

That way we can avoid running the querySelector call if we enter the first if condition.

nonNetworkedParent.appendChild(entity);
} else {
this.addEntityToSceneRoot(entity);
}
Expand Down
13 changes: 9 additions & 4 deletions src/components/networked.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ AFRAME.registerComponent('networked', {
schema: {
template: {default: ''},
attachTemplateToLocal: { default: true },
allowNonNetworkedParent: { default: false },
persistent: { default: false },

networkId: {default: ''},
Expand Down Expand Up @@ -158,7 +159,7 @@ AFRAME.registerComponent('networked', {
// Fill cachedElements array with null elements
this.invalidateCachedElements();

this.initNetworkParent();
this.initParent();

let networkId;

Expand Down Expand Up @@ -237,10 +238,13 @@ AFRAME.registerComponent('networked', {
return !!this.el.firstUpdateData;
},

initNetworkParent: function() {
initParent: function() {
var parentEl = this.el.parentElement;
if (parentEl['components'] && parentEl.components['networked']) {
this.parent = parentEl;
} else if (parentEl['components'] && this.data.allowNonNetworkedParent && parentEl.id) {
// Parent is non-networked
this.parent = parentEl;
} else {
this.parent = null;
}
Expand Down Expand Up @@ -454,12 +458,13 @@ AFRAME.registerComponent('networked', {
},

getParentId: function() {
this.initNetworkParent(); // TODO fix calling this each network tick
this.initParent(); // TODO fix calling this each network tick
if (!this.parent) {
return null;
}
var netComp = this.parent.getAttribute('networked');
return netComp.networkId;
// If the parent is networked return the networkedId, otherwise the html element id.
return netComp ? netComp.networkId: this.parent.id;
},

/* Receiving updates */
Expand Down