The Art Collector now on VIVERSE heres a quick behind the scenes

Recently I'v been working with HTC VIVERSE throught their creator program to port The Art Collector to the platform. 

https://create.viverse.com/KG6XZmH

I thoguht id do a blog post here where iv not posed in years as this was the prect this blog was created for. 



I built the prot in play canvas. An online gaem engine editor to build games for web and xr. 

https://playcanvas.com/
https://docs.viverse.com/playcanvas-sdk/playcanvas-extension-setup

Viverse has a plugin to publish to their paltform with some social features like avatar loading and chat system etc. It also has some nocodce blocks u can use to make interactions and even basic networkign on objects. 


Iv been using Deepseek ai to help with coding for any extra scripts i need. 
https://chat.deepseek.com/  There are some features that dont work in VIVERSE that do in play canvas so so its good to keep testing on both if possible. 

heres the pop art maze from the top. all the rooms exist and each door just has a teleport viverse scriptthat loops the player back to the firts room .

for the screen space popart texture i copiedthe example from viverse  and modified it a little.
Shader ARt ShuShu
https://github.com/VIVERSE-DOCS/viverse-docs/tree/main/samples




I didint get any of the shadow maskign stuff over for this port but will look into easyer ways to amke shaders in future  too for playcanvas. 
That being said VIVerse now suports other engines like unity so might not be much of an issue if u switch to those.

heres the water color level. i redesigned it to spread the wolves aroudn and give it a bit more verticality and exploration. Also wanted to amke a waterfall so added that too. 

i used this script for billboards but couldnt figure out how to make it not constrain to just 1 axis
var Billboard = pc.createScript('billboard');

// initialize code called once per entity
Billboard.prototype.initialize = function() {
    this.camera = this.app.root.findByName('PLAYER');  // Find the camera entity (PLAYER)
};

// update code called every frame
Billboard.prototype.update = function(dt) {
    // Get the direction vector from the sprite to the camera
    var direction = this.camera.getPosition();
    this.entity.lookAt(direction);
};


The water river uv script is here:
var UvScroller = pc.createScript('uvScroller');

UvScroller.attributes.add('speedU', {
    type: 'number',
    default: 0.1,
    title: 'U Speed'
});

UvScroller.attributes.add('speedV', {
    type: 'number',
    default: 0.0,
    title: 'V Speed'
});

UvScroller.attributes.add('materialSlot', {
    type: 'number',
    default: 0,
    title: 'Material Slot'
});

UvScroller.attributes.add('debugLog', {
    type: 'boolean',
    default: false,
    title: 'Debug Log'
});

// initialize code called once per entity
UvScroller.prototype.initialize = function() {
    this.offset = new pc.Vec2();
    this.material = null;
    this.meshInstance = null;
   
    // Try to get mesh instance
    if (this.entity.model) {
        if (this.debugLog) console.log("Found Model component");
        this.meshInstance = this.entity.model.meshInstances[this.materialSlot];
    } else if (this.entity.render) {
        if (this.debugLog) console.log("Found Render component");
        this.meshInstance = this.entity.render.meshInstances[0];
    }
   
    if (!this.meshInstance) {
        console.warn("UV Scroller: No mesh instance found on entity", this.entity.name);
        this.enabled = false;
        return;
    }
   
    // Get material and clone it
    this.originalMaterial = this.meshInstance.material;
    if (!this.originalMaterial) {
        console.warn("UV Scroller: No material found on mesh instance");
        this.enabled = false;
        return;
    }
   
    this.material = this.originalMaterial.clone();
    this.meshInstance.material = this.material;
   
    if (this.debugLog) {
        console.log("UV Scroller initialized on", this.entity.name);
        console.log("Using material:", this.material.name);
    }
};

// update code called every frame
UvScroller.prototype.update = function(dt) {
    if (!this.material) return;
   
    // Update offset
    this.offset.x += this.speedU * dt;
    this.offset.y += this.speedV * dt;
   
    // Wrap offset
    this.offset.x = this.offset.x % 1;
    this.offset.y = this.offset.y % 1;
   
    // Apply to material
    this.material.diffuseMapOffset = new pc.Vec2(this.offset.x, this.offset.y);
    this.material.update();
};

// Clean up cloned material when script is removed
UvScroller.prototype.onDestroy = function() {
    if (this.meshInstance && this.originalMaterial) {
        this.meshInstance.material = this.originalMaterial;
    }
};

I added a bridge over the train track to give the player more to walk in this level and these bridges are cool. 

heres the script i used for rotating objects. defo has some werid constrains i had to fight with 
// File: rotator.js
var Rotatobj = pc.createScript('rotatobj');

// Add attributes for editor configuration
Rotatobj.attributes.add('speedX', {
    type: 'number',
    default: 0,
    title: 'X Axis Speed',
    description: 'Rotation speed around the X axis (degrees per second)'
});

Rotatobj.attributes.add('speedY', {
    type: 'number',
    default: 10,
    title: 'Y Axis Speed',
    description: 'Rotation speed around the Y axis (degrees per second)'
});

Rotatobj.attributes.add('speedZ', {
    type: 'number',
    default: 0,
    title: 'Z Axis Speed',
    description: 'Rotation speed around the Z axis (degrees per second)'
});

Rotatobj.attributes.add('smoothness', {
    type: 'number',
    default: 5,
    min: 1,
    max: 20,
    title: 'Smoothness',
    description: 'How smooth the rotation should be (higher = smoother)'
});

// initialize code called once per entity
Rotatobj.prototype.initialize = function() {
    this.targetRotation = new pc.Vec3();
    this.currentRotation = new pc.Vec3();
};

// update code called every frame
Rotatobj.prototype.update = function(dt) {
    // Update target rotation based on speed
    this.targetRotation.x += this.speedX * dt;
    this.targetRotation.y += this.speedY * dt;
    this.targetRotation.z += this.speedZ * dt;
   
    // Smoothly interpolate to the target rotation
    this.currentRotation.x = pc.math.lerp(this.currentRotation.x, this.targetRotation.x, dt * this.smoothness);
    this.currentRotation.y = pc.math.lerp(this.currentRotation.y, this.targetRotation.y, dt * this.smoothness);
    this.currentRotation.z = pc.math.lerp(this.currentRotation.z, this.targetRotation.z, dt * this.smoothness);
   
    // Apply the rotation to the entity
    this.entity.setEulerAngles(this.currentRotation.x, this.currentRotation.y, this.currentRotation.z);
};


The pen level got expanded into a chase rather than a arena fight. 
I made these waypoint markers and a script to move the enemy 



--
heres the waypoint script. it has options to turn off and on multiple objects when reaches end or to loop at the end. 

var WaypointMover = pc.createScript('waypointMover');

// Add attributes that are editable in the editor
WaypointMover.attributes.add('targets', {
    type: 'entity',
    array: true,
    title: 'Target Objects',
    description: 'Array of target objects to move between'
});

WaypointMover.attributes.add('speed', {
    type: 'number',
    default: 1,
    title: 'Movement Speed',
    description: 'How fast the object moves between targets'
});

WaypointMover.attributes.add('loop', {
    type: 'boolean',
    default: true,
    title: 'Loop Targets',
    description: 'Whether to loop back to the first target after reaching the last one'
});

WaypointMover.attributes.add('stopAtEnd', {
    type: 'boolean',
    default: false,
    title: 'Stop At End',
    description: 'Stop moving when reaching the last target (overrides Loop if both are true)'
});

WaypointMover.attributes.add('proximityThreshold', {
    type: 'number',
    default: 0.1,
    title: 'Proximity Threshold',
    description: 'How close the object needs to be to consider it "reached" the target'
});

WaypointMover.attributes.add('enableAtEnd', {
    type: 'entity',
    array: true,
    title: 'Enable At End',
    description: 'Objects to enable when reaching the last target with Stop At End enabled'
});

WaypointMover.attributes.add('disableAtEnd', {
    type: 'entity',
    array: true,
    title: 'Disable At End',
    description: 'Objects to disable when reaching the last target with Stop At End enabled'
});

// initialize code called once per entity
WaypointMover.prototype.initialize = function() {
    this.currentTargetIndex = 0;
    this.isMoving = true;
    this.hasReachedEnd = false;
   
    // Check if we have any targets
    if (this.targets.length === 0) {
        console.warn('No targets specified for WaypointMover');
        this.isMoving = false;
    }
};

// update code called every frame
WaypointMover.prototype.update = function(dt) {
    if (!this.isMoving || this.targets.length === 0) return;
   
    // Get current target
    var currentTarget = this.targets[this.currentTargetIndex];
    if (!currentTarget) return;
   
    // Snap rotation to face target immediately
    this.lookAtTarget(currentTarget);
   
    // Calculate direction to target
    var direction = currentTarget.getPosition().clone().sub(this.entity.getPosition());
    var distance = direction.length();
   
    // Check if we've reached the target
    if (distance < this.proximityThreshold) {
        this.moveToNextTarget();
        return;
    }
   
    // Normalize direction and move
    direction.normalize();
    this.entity.translate(direction.scale(this.speed * dt));
};

WaypointMover.prototype.lookAtTarget = function(target) {
    var position = this.entity.getPosition();
    var targetPosition = target.getPosition();
   
    // Calculate direction to target
    var direction = new pc.Vec3();
    direction.sub2(targetPosition, position).normalize();
   
    // Calculate the angle in radians (only around Y-axis)
    var angle = Math.atan2(-direction.x, -direction.z);
   
    // Set the rotation (Y-axis only, keeping X and Z at 0)
    this.entity.setEulerAngles(0, angle * pc.math.RAD_TO_DEG, 0);
};

WaypointMover.prototype.moveToNextTarget = function() {
    // Check if we're at the last target
    if (this.currentTargetIndex === this.targets.length - 1) {
        // Handle end of path
        if (this.stopAtEnd && !this.hasReachedEnd) {
            this.isMoving = false;
            this.hasReachedEnd = true;
            this.handleEndObjects();
        } else if (!this.loop || this.stopAtEnd) {
            this.isMoving = false;
        }
    }
   
    // Move to next target (with looping if enabled)
    this.currentTargetIndex++;
    if (this.currentTargetIndex >= this.targets.length) {
        if (this.loop && !this.stopAtEnd) {
            this.currentTargetIndex = 0;
        } else {
            this.isMoving = false;
        }
    }
};

WaypointMover.prototype.handleEndObjects = function() {
    // Enable objects in the enableAtEnd array
    for (var i = 0; i < this.enableAtEnd.length; i++) {
        if (this.enableAtEnd[i]) {
            this.enableAtEnd[i].enabled = true;
        }
    }
   
    // Disable objects in the disableAtEnd array
    for (var j = 0; j < this.disableAtEnd.length; j++) {
        if (this.disableAtEnd[j]) {
            this.disableAtEnd[j].enabled = false;
        }
    }
};

// Public method to restart movement from the beginning
WaypointMover.prototype.restart = function() {
    this.currentTargetIndex = 0;
    this.isMoving = true;
    this.hasReachedEnd = false;
   
    // Reverse any end object changes
    if (this.stopAtEnd) {
        for (var i = 0; i < this.enableAtEnd.length; i++) {
            if (this.enableAtEnd[i]) {
                this.enableAtEnd[i].enabled = false;
            }
        }
        for (var j = 0; j < this.disableAtEnd.length; j++) {
            if (this.disableAtEnd[j]) {
                this.disableAtEnd[j].enabled = true;
            }
        }
    }
};

// Public method to pause/resume movement
WaypointMover.prototype.setMoving = function(moving) {
    this.isMoving = moving;
};


heres the texture swaper script i used for the pen sketchyt thing , it has slots for tiexteus and switches based on the interval in the editor. 
NOTE: this dosnt seem to work on images with the dont preload option

var TextureSwapper = pc.createScript('textureSwapper');

TextureSwapper.attributes.add('swapInterval', {
    type: 'number',
    default: 2,
    title: 'Swap Interval (seconds)',
    description: 'Time between texture swaps in seconds'
});

TextureSwapper.attributes.add('textures', {
    type: 'asset',
    assetType: 'texture',
    array: true,
    title: 'Textures',
    description: 'Array of textures to cycle through'
});

// initialize code called once per entity
TextureSwapper.prototype.initialize = function() {
    this.currentIndex = 0;
    this.timeElapsed = 0;
    this.material = null;
   
    // Find the first material component on this entity or its children
    var renderComponent = this.entity.render || this.entity.findComponent('render');
    if (renderComponent) {
        this.material = renderComponent.material;
    } else {
        console.warn('No render component found on entity or its children');
    }
   
    // Check if we have textures to work with
    if (this.textures.length === 0) {
        console.warn('No textures assigned to the texture swapper');
    }
};

// update code called every frame
TextureSwapper.prototype.update = function(dt) {
    if (!this.material || this.textures.length === 0) return;
   
    this.timeElapsed += dt;
   
    if (this.timeElapsed >= this.swapInterval) {
        this.timeElapsed = 0;
        this.swapTexture();
    }
};

TextureSwapper.prototype.swapTexture = function() {
    // Increment index and wrap around
    this.currentIndex = (this.currentIndex + 1) % this.textures.length;
   
    // Get the next texture asset
    var textureAsset = this.textures[this.currentIndex];
    if (!textureAsset || !textureAsset.resource) {
        console.warn('Texture asset not loaded or invalid at index', this.currentIndex);
        return;
    }
   
    // Apply the new texture
    this.material.diffuseMap = textureAsset.resource;
    this.material.update();
   
    // Optional: emit an event when texture changes
    this.fire('texture:changed', this.currentIndex);
};



I also ported the bib goes home projection mapped popup book game to VIVERSE
Play it here:

https://create.viverse.com/mJLmWUZ




Thanks Hope this helps some people
Happy to update this post with any more scripts peopl may want to check out. 

Till next time
Ally

Comments

Popular Posts