Keyboard Interaction of Objects in View

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

Holy crap, thank you so much @Leonidas!

The script works as expected, and I really appreciate your help. I see now how the placement of the rotate command impacts it’s behavior. One question at an engine level, multiplying the rotation by dt, appears to be the determining factor in keeping the velocity flat. Why is that? Now I know it has to be done, but understanding the concept I’m sure would go a long way for me. Does multiplying by dt take into account real time and not necessarily just raw frame production?

2 Likes

The reason is that this code moves the object every frame, but the time it takes to generate each frame isn’t constant. Sometimes it’s longer other times it’s shorter.

So if you translate your object by a constant 1 unit every frame, it’s velocity won’t be constant and the animation won’t be smooth.

The dt variable is the time it took to generate this frame since the previous one, so by multiplying our translation with it ensures that the amount the object moves each frame is constant.

1 Like

Posting for reference the proposed way on doing a visibility check on a mesh instance:

var MeshVisScript = pc.createScript('meshVisScript');

MeshVisScript.prototype.initialize = function() {
 
    this.app.scene.layers.getLayerByName("World").onPostCull = (cameraIndex) => {
        var entity = this.app.root.findByName("Box");
        var meshInstance = entity.model.model.meshInstances[0];
        console.log("visible: " + meshInstance.visibleThisFrame);
    }    
};
2 Likes