Hello there! I’m currently working in a large playcanvas project, and on it i have an inventory item system. And when the Player throw the item it’s spawned at the player position, and that’s the problem! When the player spawns some item. The item collision just stop working, and i try to figure out if it’s my fault and i cannot see any error that i’ve done. Down here u can see the related codes to the error:
/**
* ResourceManager is a singleton class responsible for loading assets in the application.
* It ensures that only one instance of the loader is created and provides methods to load assets by tag and name.
*/
class ResourceManager {
/** @type {ResourceManager|null} @private */
static #instance = null;
/**
* Creates an instance of ResourceManager.
* This constructor should not be called directly. Instead, use the `ResourceManager.instance` getter or `ResourceManager.initialize(app)` method.
*
* @param {pc.Application} app - The PlayCanvas application instance.
* @throws {Error} If an instance of ResourceManager already exists.
* @private
*/
constructor(app) {
if (ResourceManager.#instance)
throw new Error("Use the ResourceManager.instance getter to access the singleton instance.");
/** @type {pc.Application} */
this.app = app;
}
/**
* Returns the singleton instance of ResourceManager.
*
* @returns {ResourceManager} The singleton instance.
* @throws {Error} If the instance is not yet initialized.
* @static
*/
static get instance() {
if (!ResourceManager.#instance)
throw new Error("ResourceManager instance is not yet initialized. Please call ResourceManager.initialize(app) first.");
return ResourceManager.#instance;
}
/**
* Initializes the singleton instance of ResourceManager with the given app.
* If the instance is already initialized, it returns the existing instance.
*
* @param {pc.Application} app - The PlayCanvas application instance.
* @returns {ResourceManager} The initialized instance.
* @throws {Error} If the app parameter is not provided.
* @static
*/
static initialize(app) {
if (!ResourceManager.#instance) {
if (!app)
throw new Error("App parameter is required for initialization.");
ResourceManager.#instance = new ResourceManager(app);
}
return ResourceManager.#instance;
}
/**
* Configures the ResourceManager by initializing it with the provided app.
*
* @param {pc.Application} app - The PlayCanvas application instance.
* @static
*/
static configure(app){
ResourceManager.initialize(app);
}
/**
* Loads an asset by its tag and name.
*
* @param {string} assetTag - The tag of the asset, which must be one of the predefined tags in the TAGS class.
* @param {string} assetName - The name of the asset.
* @returns {*} The loaded asset resource.
* @throws {Error} If the asset tag is invalid or if the asset cannot be found.
*/
loadAsset(assetTag, assetName) {
if (!Object.values(TAGS).includes(assetTag))
throw new Error(`Invalid assetTag: ${assetTag}. It must be one of the predefined tags in the TAGS class.`);
const asset = this.app.assets.findByTag(assetTag)
.filter((asset) => asset.name === assetName)[0];
if (!asset)
throw new Error(`Asset with name "${assetName}" and tag "${assetTag}" not found.`);
return asset;
}
/**
* Provides access to runtime operations related to entities.
* This getter returns an object with methods for loading and interacting with entities at runtime.
*
* @returns {Object} An object with runtime operations.
*/
get runtime() {
const app = this.app;
return {
/**
* Loads all entities with the specified tag at runtime.
*
* @param {string} TAG - The tag of the entities to load.
* @returns {pc.Entity[]} An array of entities that have the specified tag.
* @throws {Error} if the tag is invalid
*/
loadEntities(TAG) {
if (!Object.values(TAGS).includes(TAG))
throw new Error(`Invalid entityTag: ${TAG}. It must be one of the predefined tags in the TAGS class.`);
return app.root.findByTag(TAG);
}
};
}
}
application.addDependency(ResourceManager);
Ignore the application addDependency, it is just saying to an extern class to call this class configure when the game loads.
/**
* The `TemplateLoader` class is responsible for loading and instantiating template assets
* in a PlayCanvas application. It provides methods to load the template asset asynchronously
* and to spawn a new instance of the template in the scene.
*/
class TemplateLoader {
/**
* Creates an instance of `TemplateLoader`.
*
* @param {string} templateName - The name of the template asset to load.
*/
constructor(
app,
templateName
){
this.app = app;
/**
* @type {string}
* @description The name of the template asset to load.
*/
this.templateName = templateName;
/**
* @type {pc.Asset|null}
* @description The loaded template asset.
* @private
*/
this._templateAsset = null;
this._loadTemplate();
}
/**
* Loads the template asset.
* @returns {pc.Asset} The loaded template asset.
*/
_loadTemplateAsset = () => {
return ResourceManager.instance
.loadAsset(
TAGS.RUNTIME_TEMPLATE,
this.templateName
);
}
/**
* Asynchronously loads the template asset and sets it to the `templateAsset` property.
*
* This method calls the `loadTemplateAsset` function to retrieve the template asset, and
* then assigns the resource of the loaded template to the `templateAsset` property of the instance.
*
* @returns {Promise<void>} A promise that resolves once the template asset has been loaded and set.
*/
_loadTemplate = async () => {
const template = await this._loadTemplateAsset();
this._templateAsset = template.resource;
}
/**
* Spawns a new template in the scene.
*
* This method attempts to instantiate a new template from the preloaded template asset.
* The newly created template is passed to the provided callback function for further
* configuration. The callback function **must** accept the newly instantiated template
* as its parameter.
*
* If the template asset is not loaded or any errors occur during the process,
* the error is logged, and the function returns `false`.
*
* @param {Function} configureTemplateCallback - A callback function that takes the newly
* instantiated template as a parameter. This callback is used to configure the template
* (e.g., setting its position, rotation, or other properties).
* The callback **must** accept one argument, which is the instantiated template.
*
* @returns {boolean} True if the template was successfully instantiated and added to the scene,
* false otherwise.
*/
spawnTemplate = (
configureTemplateCallback,
spawnAt = this.app.root
) => {
try {
if (!this._templateAsset) {
console.error('Template asset not loaded');
console.trace();
return false;
}
const newTemplate = configureTemplateCallback(this._templateAsset.instantiate());
spawnAt.addChild(newTemplate);
return true;
} catch (error) {
console.error('Error spawning template:', error);
console.trace();
return false;
}
}
}
Class to load runtime templates in my app, is simple the template must have the tag runtime-template, so it can be found in this class and loaded.
/**
* Class representing an Inventory.
*/
class InventoryManager {
.... Lot of inventory logic here.
this.inventoryRender = new InventoryRender(
this.backgroundItems,
this
); // this is called at constructor
/**
* Spawns a new item in the scene using the specified item data.
*
* This method instantiates a new item from a template asset, sets its position and rotation
* to match the current entity, and initializes its script component with provided item details.
* If any errors occur during this process, they are logged to the console.
*
* @param {Object} item - The data for the item to be spawned.
* @param {string} item.name - The name of the item.
* @param {string} item.spriteName - The name of the sprite associated with the item.
* @param {string} item.itemInteractionName - The name of the function to be used for item interaction.
* @returns {boolean} True if the item was successfully spawned, false otherwise.
*/
spawnItem = (item) => {
return this.templateLoader.spawnTemplate((newItem) => {
newItem.setPosition(this.entity.getPosition());
newItem.setRotation(this.entity.getRotation());
const script = newItem.script;
if (!script || !script.item)
throw ('Script or item script component not found on the new item');
script.item.setItem(item);
return newItem;
})
}
/**
* Removes an item from the specified position in the backgroundItems array.
* @param {Array} backgroundItems - The 2D array representing the inventory grid.
* @param {number} rowIdx - The row index of the item.
* @param {number} colIdx - The column index of the item.
*/
removeItem = (backgroundItems, rowIdx, colIdx) => {
this.executeWithUIUpdate(() => {
if (backgroundItems[rowIdx] && backgroundItems[rowIdx][colIdx]){
const itemData = backgroundItems[rowIdx][colIdx];
const item = itemData.item;
--itemData.quantity;
const isItemSpawned = this.inventoryManager.spawnItem(item);
if(!isItemSpawned)return;
if(itemData.quantity > 0)return;
backgroundItems[rowIdx][colIdx] = null;
}
});
}
All this code works! And the item’s spawned as it should. But the first problem appears… The collision of the item do not work anymore and if i try to change scene sometimes the playcanvas crashes in “Component.js”. I don’t know what i’ve did done wrong, or even this problem is my fault, but to be honest i really think this problem is my fault and i did something wrong.
My item logic:
var Item = pc.createScript('item');
Item.attributes.add("itemName", {
title: 'Item name to be rendered at inventory',
type: "string"
});
Item.attributes.add('descriptionName', {
type: 'string',
description: "Description name to be loaded at inventory"
});
Item.attributes.add('spriteName', {
type: 'string',
description: "Sprite name to be rendered at inventory"
});
Item.attributes.add('functionName', {
type: 'string',
title: 'interact Function Name'
});
Item.attributes.add('isUnique', {
type: 'boolean',
title: 'Is this item unique?',
default: true
});
Item.attributes.add('isStateless', {
type: 'boolean',
title: 'Is this item stateless?',
default: false
});
Item.attributes.add('isConsumable', {
type: 'boolean',
title: 'Is this item consumable?',
default: true
});
Item.attributes.add('uses', {
type: 'number',
title: 'If consumable how many uses the it have?',
default: 1
});
Item.prototype.initialize = function() {
this.item = this.item || new InventoryItem({
guid: this.entity.getGuid(),
name: this.itemName,
descriptionName: this.descriptionName,
spriteName: this.spriteName,
interactionName: this.functionName,
isUnique: this.isUnique,
isStateless: this.isStateless,
isConsumable: this.isConsumable,
uses: this.uses
});
({
name: this.itemName,
isUnique: this.isUnique,
isStateless:
this.isStateless
} = this.item);
if (!ItemStateManager.shouldRenderItem(this.item.guid, this.isStateless)) {
this.entity.enabled = false;
return;
}
this.entity.action = (player) => {
player.fire(EVENTS.inventory.INSERTITEM, this.item);
ItemStateManager.collectItem(this.item);
this.entity.enabled = false;
}
};
Item.prototype.setItem = function(item) {
this.item = item;
}
I don’t know if i used guid correctly. But i only read the guid…
Below a video of the problem:
Weird
The code and all the item stuff was working yesterday 09/04/2024, and the only thing i’ve changed is the item attributes to have the isConsumable and the uses.
Things i’ve tried
- Check if template is loading correctly: Yes, it’s, and the collision component and all the scripts is initializing.
- Try to not use guid in any part of code: Nothing change, i’m only reading it but i have some fear of using guid.
- Try to remove the isConsumable and the uses: Nothing change, just my inventory logic to consume the item… But the bug still there.
I don’t know if playcanvas updated today or if i did something wrong in my code… But i hope someone can help me.