Keyboard Interaction of Objects in View

I hope that this is in the right place.

As I’m familiarizing myself with the engine, I’m finding it hard to locate specific functions or API’s that should be called upon. So some help or guidance in the right direction would be greatly appreciated!

The scope of the project is to have an environment with static rigid bodies, some of which can be interacted with to generate a small window with additional information. The project is in first person, and I have the first person movement and rigid bodies set up. I know that it is possible to generate HTML pages with CSS styling.

My question is if I have several objects in an area, and I use the same interaction button, what would be the best way to select the closest object in the player view. Are there particular functions that might help me while experimenting? I basically want to have an object highlight when looked at within a certain radius of the player and respond to an action button. I’m getting a bit stuck on how the view based selection might be scripted.

Any guidance would be greatly appreciated.

Hi @russia213 and welcome!

A good and performant way to do it is to create a list (array) of all the entities that you would like to check if they are close or not and do the following tests:

  • If they are inside the player’s view, using frustum culling for that. Make sure it’s enabled on your active camera and then you can run the following check for each entity:
entity.model.meshInstances.forEach(function(meshInstance){
   if( meshIntance.visible === true){
      // entity is inside the player's view
   } 
});
  • From the visible entities you can easily now find the closest one by running a simple loop to get the closest visible entity to the camera:
this.vec = new pc.Vec3();
var cameraPos = activeCamera.getPosition();

var minDistance = Infinity;
var closestEntity;

visibleEntities.forEach( function(entity){

   this.vec.copy(entity.getPosition());
   var distance = this.vec.distance(cameraPos);

   if(distance < minDistance){
      minDistance = distance;
      closestEntity = entity;
   }
}); 

// closestEntity is the closest visible entity to the player's view.

Hi @Leonidas,

Thank you so much for the feedback. This definitely seems to be the logic that I’m looking for. I guess I’m having trouble wrapping my head around the process of the picking of entities. Would I apply separate scripts to each interactable object, or would this be a standalone script applied to the root?

Starting with the first test, I’m having trouble understanding how to define the entity variable to apply to all entities that might come into view.

I apologize for the probably simple questions. I’ve worked within php before where everything was much more static than it is here. If you’re willing to give any more advice I would surely appreciate it.

Hi @russia213,

Indeed coming from a static/event based system like frontend/backend web applications to 3D requires a shift in logic, but you are getting there! Asking the right questions.

In your Playcanvas editor you add the objects to be rendered in your scene Hierarchy as entities (left panel). Each entity added has a name and can also have a number of tags. These are your first tools on how to select and filter your entities in code.

image

// how to get a reference to the entity named Camera
var cameraEntity = this.app.root.findByName('Camera');

// how to get a list (array) of all entities that have the tag 'main-camera'
var cameraEntities = this.app.root.findByTag('main-camera');

So, similarly in your project you can tag all your entities that you would like to run the test on (e.g. ‘enemy’) and do the following:

var enemies = this.app.root.findByTag('enemy');
var visibleEnemies = [];

enemies.forEach(function(enemy){
   entity.model.meshInstances.forEach(function(meshInstance){
      if( meshIntance.visibleThisFrame === true && visibleEnemies.indexOf(enemy) === -1){
         // entity is inside the player's view
         visibleEnemies.push(enemy);
      } 
  });
});

This way you can loop through your broadenemies list and create a new list that contains only the visible ones.

Thank you again @Leonidas

I took some time to tinker with the suggested code. At the moment I do not have this assigned as a component script for any entity. The ‘enemy’ tag is assigned to 3 block primitives for testing.

At first I was receiving an ‘entity is not defined’ error. I pushed the enemies array to console and changed the code around a bit to try and point myself in the right direction. Here is the entire script I’m playing with for the moment:

var EntitiesInView = pc.createScript('entitiesInView');



// initialize code called once per entity
EntitiesInView.prototype.initialize = function() {

    
};

// update code called every frame
EntitiesInView.prototype.update = function(dt) {
    
    
    
var enemies = this.app.root.findByTag('enemy');
var visibleEnemies = [];
    


enemies.forEach(function(enemy){
   enemy.model.model.meshInstances.forEach(function(meshInstance){
      if( meshInstance.visible === true && visibleEnemies.indexOf(enemy) === -1){
         // entity is inside the player's view
         visibleEnemies.push(enemy);
          console.log(visibleEnemies);
      } 
  });
});
    
};

It seemed the only way to get to the mesh instances within the array was to go further into the 2nd ‘model’ array. I then pushed the visibleEnemies array to the console for testing. I noticed that even while looking away from the objects, it would continue pushing the array to the console each frame. I have a sneaking suspicion that I’m missing something here.

As always, additional support is greatly appreciated.

Good, let’s see:

  • The model.model.meshInstances sometimes is required when using non asset models. Good that you found it.
  • For the objects being always visible, do you have frustum culling enabled in your active camera? It should be enabled to do trigger the visible flag.

If you are still experiencing issues, can you share a public project to take a look?

I checked the camera, and it is indeed set to have Frustum Culling enabled. Here is a link to the project:

https://playcanvas.com/project/673634/overview/first-person-test

As always, your feedback is greatly appreciated.

Your code and scene setup are correct, apart from a small change that I didn’t get right above:

But still you are right, that property always returns true which isn’t correct, seems like a bug.

I’ve submitted an issue to the Playcanvas bug tracker about it, let’s see what’s the problem here:

It’s good to know I’m at least going in the right direction. To try and keep some semblance of progress, I’m trying to go about it a different way and getting some unexpected results. I forked the project to try a different strategy. The new script I’m working with is:

var EntitiesInView = pc.createScript('entitiesInView');



// initialize code called once per entity
EntitiesInView.prototype.initialize = function() {


};

// update code called every frame
EntitiesInView.prototype.update = function(dt) {
    
    
    
var projectionMatrix = this.entity.camera.projectionMatrix;
var viewMatrix = this.entity.camera.viewMatrix;
var frustum = new pc.Frustum(projectionMatrix, viewMatrix);
frustum.update(projectionMatrix, viewMatrix);  
    // console.log(frustum);
 
var enemies = this.app.root.findByTag('enemy');
var visibleEnemies = [];
    

enemies.forEach(function(enemy){
   enemy.model.model.meshInstances.forEach(function(meshInstance){
       
      
       var enemyPos = enemy.getPosition();
       
           if (frustum.containsPoint(enemyPos) === true){
        console.log("I see you!");
    }
    
  });
});
    
};

I’m getting some interesting results. Now it is definitely dependent on where the camera is facing, but despite updating the frustum each frame, I only return the “I see you” log based on where the camera is facing during the first frame. Movement afterward appears to have no effect.

Any ideas?

Ok! So new information! It looks like the Frustum is intersecting correctly when an object is in view, but there is a large caveat. While the position of the Frustum is linked to the active camera, it’s rotation is not. I am looking into ways to change that, but if anyone has any ideas, I would be happy to hear them.

1 Like

Thanks for sharing your research! You are actually replicating part of what the engine internally is doing with its culling pipeline in the forward renderer.

The rotation of the camera should definitely be taken into account, let’s see if the github issue sheds some light on this.

You will love this.

this.camera = null;

^ That line right there is exactly the issue with the current method testing intersection. It is line 19 of the First Person View tutorial. Even though a camera attribute is available in the script. That line completely overrides it and creates a new active camera on initialization. Deleting that line and reassigning the correct camera in the editor attributes allowed my 2nd attempt script to work as expected.

I only discovered this because of a thread from 2018: Camera Direction Vector

Thanks to @yaustar for seeing the problem in the tutorial script back then. It really seems like something that should be looked at or changed, as it is a bit confusing.

As for the original script, visibleThisFrame still continues to return true always even with the correct camera, so no luck there, but at least I’ve made a little bit of progress. Now I will return to the original interaction problem of the thread, and see what I find.

Thank you @Leonidas for your help!

Here is the working project: https://playcanvas.com/project/674259/overview/1st-person-test-continued

I will fork it again and continue my project.

Good, nice find, indeed your frustum culling script was losing the reference to the active camera after the first frame.

Happy that you moved so quickly to advanced methods, nice progress!

I am having trouble with the testing of the closest object script. For whatever reason, the primitives are spinning at different speeds, and when using a wasPressed function, all of the objects raise instead of just the closest one if they were ever the closest object at one time. Curiously, they transform at different rates. The console output makes it seem like the “closestEnemy” variable is storing multiple entries over each frame instead of clearing the information, but after hours of fighting with it, I still have no idea how to fix it. Any assistance would be greatly appreciated. Here is the code:

var EntitiesInView = pc.createScript('entitiesInView');




// initialize code called once per entity
EntitiesInView.prototype.initialize = function() {

    
};

// update code called every frame
EntitiesInView.prototype.update = function(dt) {
     
    this.app.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown, this);
    this.app.keyboard.on(pc.EVENT_KEYUP, this.onKeyUp, this);
    var projectionMatrix = this.entity.camera.projectionMatrix;
    var viewMatrix = this.entity.camera.viewMatrix;
    var frustum = new pc.Frustum(projectionMatrix, viewMatrix);
    var enemies = this.app.root.findByTag('enemy');
    var visibleEnemies = [];
    var cameraPos = this.entity.getPosition();


    enemies.forEach(function(enemy){
       enemy.model.model.meshInstances.forEach(function(meshInstance){
           
           var enemyPos = enemy.getPosition();

               if (frustum.containsPoint(enemyPos) === true){
                    visibleEnemies.push(enemy);

	        var minDistance = 5;
	        var closestEnemy;
	        var vec = new pc.Vec3();
	        var distances = [];

	        visibleEnemies.forEach( function(visible_enemy){
	            vec.copy(visible_enemy.getPosition());
	            var distance = vec.distance(cameraPos);
	            distances.push(distance);
	            var shortest = Math.min(distances);


	            if (distance <= shortest && distance <= minDistance) {
	                shortest = distance;
	                closestEnemy = visible_enemy;
	                
	                // console.log(closestEnemy);
	                
	                closestEnemy.rotate(0, 1, 0);
	                
	                    EntitiesInView.prototype.onKeyDown = function (event) {
	                        // Check event.key to detect which key has been pressed
	                        if (this.app.keyboard.wasPressed(pc.KEY_Y)) {
	                            closestEnemy.translate(0, 0.001, 0);
	                            // console.log(closestEnemy);
	                        }

						};
	            }

	        });

                }
        
        });
        

    });


    
};

// swap method called for script hot-reloading
// inherit your script state here
// EntitiesInView.prototype.swap = function(old) { };

// to learn more about script anatomy, please read:
// http://developer.playcanvas.com/en/user-manual/scripting/

Hi @russia213,

You are indeed getting close, you just need to rearrange some things:

  • You need to move the following event handlers outside of your update loop. Those need to be attached once and they will automatically fire back each time a key is pressed.
  • The event handler callback method (onKeyDown) should be declared outside of your update loop, much like the initialize and update methods do.

Here is your script rewritten, with some extra changes to the distance calc method. I haven’t tested it but you can get the generic idea:

var EntitiesInView = pc.createScript("entitiesInView");

// initialize code called once per entity
EntitiesInView.prototype.initialize = function() {
  this.vec = new pc.Vec3();

  this.app.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown, this);
};

// update code called every frame
EntitiesInView.prototype.update = function(dt) {
  var projectionMatrix = this.entity.camera.projectionMatrix;
  var viewMatrix = this.entity.camera.viewMatrix;
  var frustum = new pc.Frustum(projectionMatrix, viewMatrix);
  var enemies = this.app.root.findByTag("enemy");

  this.visibleEnemies = [];

  enemies.forEach(function(enemy) {
    enemy.model.model.meshInstances.forEach(function(meshInstance) {
      var enemyPos = enemy.getPosition();

      if (frustum.containsPoint(enemyPos) === true) {
        this.visibleEnemies.push(enemy);
      }
    });
  });
};

EntitiesInView.prototype.onKeyDown = function(event) {
  // Check event.key to detect which key has been pressed
  if (this.app.keyboard.wasPressed(pc.KEY_Y)) {
    var cameraPos = this.entity.getPosition();
    var minDistance = 5;
    var closestEnemy;
    var shortest = Infinity;

    visibleEnemies.forEach(function(visible_enemy) {
      this.vec.copy(visible_enemy.getPosition());
      var distance = this.vec.distance(cameraPos);

      if (distance <= shortest && distance <= minDistance) {
        shortest = distance;
        closestEnemy = visible_enemy;
      }
    });

    if (closestEnemy) {
      closestEnemy.rotate(0, 1, 0);
      closestEnemy.translate(0, 0.001, 0);
    }
  }
};

As a performance optimization, you could calculate the visible entities not per frame (in your update loop) but only when needed (e.g. when the key down is executed).

1 Like

Hi @Leonidas,

I’m testing the script above. Thank you very much for the rearrangement. Upon looking at the specified targets, I’m receiving a "this.visibleEnemies is undefined " error in console. I see it is defined outside of the forEach function, but I’m not sure on the logistics of making the array available inside of the function.

I moved some things around in the rewritten script and got things sort of working. I no longer get undefined errors, and only the closest enemy is moved on key press. The only abnormality is that while the rotation works upon look, it quickly accelerates out of control. Here is the updated script:

var EntitiesInView = pc.createScript("entitiesInView");

// initialize code called once per entity
EntitiesInView.prototype.initialize = function() {


  this.app.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown, this);
};
  this.vec = new pc.Vec3();
  this.visibleEnemies = [];
  var closestEnemy;

// update code called every frame
EntitiesInView.prototype.update = function(dt) {
  var projectionMatrix = this.entity.camera.projectionMatrix;
  var viewMatrix = this.entity.camera.viewMatrix;
  var frustum = new pc.Frustum(projectionMatrix, viewMatrix);
  var enemies = this.app.root.findByTag("enemy");
  var cameraPos = this.entity.getPosition();


  enemies.forEach(function(enemy) {
    enemy.model.model.meshInstances.forEach(function(meshInstance) {
      var enemyPos = enemy.getPosition();

      if (frustum.containsPoint(enemyPos) === true) {
        this.visibleEnemies.push(enemy);
      }
            
    var minDistance = 5;

    var shortest = Infinity;

    visibleEnemies.forEach(function(visible_enemy) {
      this.vec.copy(visible_enemy.getPosition());
      var distance = this.vec.distance(cameraPos);

      if (distance <= shortest && distance <= minDistance) {
        shortest = distance;
        closestEnemy = visible_enemy;
        closestEnemy.rotate(0, 0.01, 0);
      }
    });
    });
  });
};

EntitiesInView.prototype.onKeyDown = function(event) {
  // Check event.key to detect which key has been pressed
  if (this.app.keyboard.wasPressed(pc.KEY_Y)) {

    if (closestEnemy) {

      closestEnemy.translate(0, 0.1, 0);
    }
  }
};

So, when using this inside methods you need to make sure to reference the right context. You do this using bind.

For the rotation spin issue, you just need to rotate only when you press the button instead of every time the entity is visible.

Here is another version of the script on how to resolve that, with some performance optimizations as well:

var EntitiesInView = pc.createScript("entitiesInView");

// initialize code called once per entity
EntitiesInView.prototype.initialize = function() {
  this.app.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown, this);

  var projectionMatrix = this.entity.camera.projectionMatrix;
  var viewMatrix = this.entity.camera.viewMatrix;
  this.frustum = new pc.Frustum(projectionMatrix, viewMatrix);

  this.enemies = this.app.root.findByTag("enemy");

  this.vec = new pc.Vec3();
  this.visibleEnemies = [];
  this.closestEnemy = undefined;
};

// update code called every frame
EntitiesInView.prototype.update = function(dt) {
  var projectionMatrix = this.entity.camera.projectionMatrix;
  var viewMatrix = this.entity.camera.viewMatrix;
  this.frustum.update(projectionMatrix, viewMatrix);

  var cameraPos = this.entity.getPosition();

  this.visibleEnemies = [];

  this.enemies.forEach(
    function(enemy) {
      var enemyPos = enemy.getPosition();

      if (this.frustum.containsPoint(enemyPos) === true) {
        this.visibleEnemies.push(enemy);
      }
    }.bind(this)
  );

  var minDistance = 5;
  var shortest = Infinity;

  this.visibleEnemies.forEach(
    function(visible_enemy) {
      this.vec.copy(visible_enemy.getPosition());
      var distance = this.vec.distance(cameraPos);

      if (distance <= shortest && distance <= minDistance) {
        shortest = distance;
        this.closestEnemy = visible_enemy;
      }
    }.bind(this)
  );

  if (this.closestEnemy) this.closestEnemy.rotate(0, 10 * dt, 0);
};

EntitiesInView.prototype.onKeyDown = function(event) {
  // Check event.key to detect which key has been pressed
  if (this.app.keyboard.isPressed(pc.KEY_Y)) {
    if (this.closestEnemy) {
      this.closestEnemy.translate(0, 0.1, 0);
    }
  }
};

Thank you again for your assistance. Knowing about the bind function is very helpful.

Regarding the spin, I am confused about the acceleration issue. I would like the object to spin continuously while being looked at and the closest designated entity. This means that only partial rotation on key press is not exactly what I’m looking for.

The end goal is to separate the actions of identification and interaction to the user. So, spin when a key press will affect this object, and then do the specified action once the key is pressed. I had been considering material changes or spinning for the identification part, so that the user is aware what they will be interacting with. I have seen in other scripts that a rotate function on update would allow for continuous, steady spin, so I’m confused why it seems to be impossible here.

Is it even possible to have a specified entity that is in frame spin steadily? I’m wondering if it has something to do with the array, this.visibleEnemies. It never seems to be cleared, so I wonder if each frame, the array grows and grows, causing this.visibleEnemies.forEach function to calculate based on multiple instances of an entity previously being the closest. That being said, attempting to clear the array at the end of the function seems to either remove all spin, or have no effect at all.

Yes, the array is cleared every frame with this line inside the update method:

this.visibleEnemies = [];

I’ve updated the script above to do what you want, have the entity rotate continuously and also I’ve updated the project you posted above:

https://playcanvas.com/editor/scene/895525

1 Like