How I created Recycle! VR



    In the previous article, we tried to create a basic scene in A-Frame in order to try out the basic concepts of the framework in practice. In this article, I would like to share my experience in creating a game on A-Frame - Recycle! VR The project repository is available at the following link .

    Recycling !?


    The idea of ​​creating a game came almost immediately as soon as I found out about Web VR. Although in general, I believe that games on the web in any case will be inferior to good projects even for mobile devices, not to mention personal computers and consoles. But, it seems to me, the game is the most difficult test. I started thinking about exactly what I can do. I watched other projects and I was immediately struck by the opportunity to take something using the controller. And since I have been connected with an organization that is engaged in separate garbage collection for quite some time, the answer came by itself. Recycling. We take the trash and throw it in the trash. What could be easier. But everything turned out to be not so simple, and this, in fact, will be discussed further.

    Framework selection


    At the time of the start of work on the game, I knew only about 2 more or less serious frameworks: React 360, A-Frame. Obviously, A-Frame was most suitable for creating the game. Yes, now I know that there is still a PlayCanvas game engine that also supports VR, but it's too late. Moreover, it turned out that A-Frame is also not bad for creating games.

    Where to begin?


    I began by studying official game examples from A-Frame developers. The benefit of these is not enough. A-Blast, A-Painter, Museum, Super Craft and now also Gunters of Oasis. Of all the projects presented, I liked A-Blast the most - a shooter in which you have to fight with the cutest creatures in the universe. Therefore, I wanted to take this game as a template for mine. But it didn’t work out. And the reason for this was the structure of the game. It seemed to me that she was too cluttered and not thought out. Perhaps more is not required, but I wanted to do something more convenient and easier to understand.

    Structure


    The A-Blast structure represents only one entry point - the index.html file, which contains one scene with all assets, basic game entities, controls, and everything in general.



    As you can see in the screenshot, in addition to the necessary components and systems (A-Frame uses the Entity Component System pattern), there are also bullets and enemies - essentially the same systems, but for some reason have their own wrapper. Generally say that this code is not easy to understand. So I decided to think about how this code can be structured. The first idea is to break up the scene into its constituent parts. Why would a router and templates be useful, which would render this or that part of the scene. After searching for the first and second (no, not five minutes), I did not find anything. Although I am a supporter of the rule, do not write bicycles, but this time I had to write my decision. Although, somewhere in 2-3 weeks I came across templates from Kevin Ngo. But it was too late.

    Router and patterns




    And so, a-frame-router-templates enters the scene . What can he do? As mentioned above, its main task is to render the necessary parts of the game, for example, the title screen, the playing field, the end screen of the game, etc. How to do it? In principle, you can find everything you need in the documentation for the module on github, but in short, we have the following:

    
        ... 
        
            
       
       ...
       
           
       
       ...
    

    1. We are adding the router component to the scene.
    2. Add a-route for each part of the application (scene's frames). One route for the home screen, another for the playing field, etc.
    3. Render templates directly through a-templates
    4. If necessary, we change routes through

      this.el.systems.router.changeRoute('game-field');

      Note : this example refers to the scene code, so we can call the router system directly.
    5. We set and connect the templates, something like this:

      
      AFRAME.registerTemplate('game-field', `
         
             ...
             
      `);
      

      Note: a-sub-assets allow you to load assets as well as a-assets, but only with the difference that there is a check by default and if the asset is already added, it will not be added again when the route is changed.

      Note 2: Normally you can use templates only with ES6 template string. Otherwise, it can turn into “string” + var + “string”, not cool. Kevin, for example, has support for template engines. But why complicate it, right?

    Thus, you can create a convenient application structure that will contain the following: components, systems, templates, states, libs . Nothing more and everything is on the shelves.

    Manipulating objects




    The very first task that was to be solved was the manipulation of objects. I needed a functional like grab - throw. Initially, I began to ponder how to create such a component from scratch. Purely on a philistine level, such a reflection is permissible: we have a controller (in the case of a desktop it is a cursor), it has a position. We also have certain objects, for example cubes, they also have a position. By changing the position of the controller, we must change the position of the object. Simply? So, actually, yes, but it won’t work. I will mention a couple of points from a very long list to convince you of this:

    • The cursor in A-Frame is a descendant of a-camera and has relative coordinates;
    • The position of the controller is not enough, you still need to consider the orientation, the distance to the object, the position of the camera (player);
    • For objects with a physical body, this will not work at all, because the coordinates of the geometry are connected with the coordinates of the body.

    It’s good that the good Mr. Wil Murphy and his friends did a-frame-super-hands . In essence, this library contains all the necessary components:

    • hoverable . Guidance. Point the controller or cursor over the collision zone of the object (usually the whole object)
    • grabbable : Capture. Grab an object using the appropriate button and drag it
    • stretchable : Grab with both hands and stretch \ squeeze
    • draggable \ dropable : Essentially needed to determine the event “the item was thrown at a specific location”

    You can find everything you need about setting up and connecting super-hands in the repository mentioned above. I just want to draw attention to a number of nuances:

    • Create separate mixins for the right and left hand. Separate components by type of supported devices. For example, the right hand, in addition to oculus-touch, vive-controls, windows-motion-controls, there may also be oculus-go-controls and gear-vr-controls. The left hand needs to be hidden for BP mobile helmets. Each controller must contain both mixin hands and a super-hands component. An example ;
    • If you specified objects: .clsname for reycaster, do not forget to add it to each element which can be taken using the controller, otherwise not one event for super-hands will fail. Of course, if colliderEvent: raycaster-intersection ;
    • Dragging with the mouse projects 2d coordinates into the 3d world, so it is better to use the cursor for the desktop.

    Add physics


    Adding physics to a-frame is actually very simple. There is a special system for this . It is added to the scene and voila, physics is already in your pocket.


    Mark : debug: true enables the ability to view physical bodies bound to geometry. It is convenient when you need to “outline” an object.

    In fact, this is a wrapper for cannon.js that does all the dirty work of comparing geometry and physical bodies for you. Again, about how this system works, you can find it in the description of the repository. And I would like to dwell on only one point that is important for my game.

    I needed to make sure that by pressing the button to the trash a certain force was set (the more you hold the button pressed, the greater the force). As it turned out, this task is not so simple as it seems at first glance. Well, what's so complicated? - you say, we do applyImpluseand voila. Not really ... It sets the rotation of an object along a vector applied to the center of the body. Using this method, we can only emulate a yule. Although, if you set a vector with the correct angle to the plane, you may get something similar to a push. But this is not what I needed.

    As it turned out, I needed speed when setting this parameter, the object begins its movement in a given direction. This direction is specified by the vector. And here the fun begins. How to find this vector? I found two options:

    1. Get the quaternion of the controller (or camera for the desktop), which describes its orientation in space. Create a vector V1 = <1,1,1>, multiply it by the throwing force and apply the orientation to all of this.

      const velocityVector = new THREE.Vector3(1,1,1);
      velocityVector.multiplyScalar(this.force);
      velocityVector.applyQuaternion(controllerQuaternion);
      this.grabbed.body.velocity.set(velocityVector.x, velocityVector.y, velocityVector.z); 
      
    2. Find the position of the controller (cursor) and the position of the object being thrown. Calculate the direction vector for two points. Normalize the vector. And multiply it by force.

      const directionX = (trashPosition.x - zeroPosition.x);
      const directionZ = (trashPosition.z - zeroPosition.z);
      const vectorsLength = Math.sqrt(Math.pow(directionX, 2) + Math.pow(directionZ, 2));
      const x = (directionX / vectorsLength) * this.force;
      const y = this.force;
      const z = (directionZ / vectorsLength) * this.force;
      this.grabbed.body.velocity.set(x , y,  z );
      

    I chose the second option because in it I can count only x and z. And set y yourself, since I needed a throw along the arc so that the dumped garbage would fall into the basket, despite the user holding the controller.

    A few words about the model




    From the very beginning, I decided to make a low-poly style game . Although WebGL is capable of rendering relatively complex scenes today, its performance is still inferior to advanced libraries such as DirectX, Vulkan, Mantle, etc. It also all depends on the performance of the user's device. Since I would like to focus on more affordable BP mobile helmets (Oculus Go, Gear VR), I think that low-poly is one of the few solutions for creating a VR application or game. Although, of course, it all depends on the volume.

    Okay, low-poly is so low-poly, but how to do it all? Everything is very simple, there is a good open source tool - Blender. Believe me, he is capable of much, but for simple tasks he is not quite suitable. There are a lot of training materials related to modeling in Blender and it will not be difficult to find them. I just wanted to focus your attention on a number of points related to web development:

    1. Three-js exporter is out of date. Need to find and supply GLTF exporter . GLTF is a special format designed for the web. And yes, this is JSON.
    2. GLTF does not support Cycles Renderer, so you have to use Blender Renderer. And this means that there will be no cool knots, color transformations, metal highlights (can be done differently).
    3. You need to export only the selected item. You do not need extra cameras and lights? File> Export> gltf 2.0. In the left menu, Export GLTF 2.0> Export selected only.
    4. We start exporting from position <0, 0, 0> in Blender. It is better to scale in the same place, so that later you do not use the scale component in a-frame.
    5. If you draw open space like in Recycle! VR, you need to add objects only to where the player can theoretically look. Behind, behind the houses, in the Recycle! there are a couple of trees and only in the place where the user can see them. It’s not necessary to overload the scene.
    6. If you need to change the model material, you need to wait until it loads, get the model itself, pull all nodes out of it (GLTF contains information not only about meshes)

      
      e.detail.model.traverse((node) => {
         if (node.isMesh) {
             node.material.color = new THREE.Color(someColor);
         }
      });
      

    Finally


    Thank you all for your attention! I remind you once again that the project repository is available at the following link . Anyone who wants to bring something new to this game - welcome.

    Also popular now: