This is the second part of our quest to create a 3D space shooter. In part one we looked at how to set up a basic PlayCanvas game, with physics and collision, our own models, and a camera.
For reference, here's a live demo of our final result again.
In this part, we're going to focus on dynamically creating entities with scripts (to spawn bullets and asteroids) as well as how to add things like an FPS counter and in-game text. If you've already followed the previous part and are happy with what you have, you can start building from that and skip the following minimum setup section. Otherwise, if you need to restart from scratch:
Minimum Setup
- Start a new project.
- Delete all objects in the scene except for Camera, Box, and Light.
- Put both the light and the camera insidethe box object in the hierarchy panel.
- Place the camera at position (0,1.5,2) and rotation (-20,0,0).
- Make sure the light object is placed in a position that looks good (I put it on top of the box).
- Attach a rigid body component to the box. Set its type to dynamic. And set its damping to 0.95 (both linear and angular).
- Attach a collision component to the box.
- Set the gravity to 0 (from the scene settings).
- Place a sphere at (0,0,0) just to mark this position in space.
- Create and attach this script to the box and call it Fly.js:
var Fly = pc.createScript('fly'); Fly.attributes.add('speed', { type: 'number', default:50 }); // initialize code called once per entity Fly.prototype.initialize = function() { }; // update code called every frame Fly.prototype.update = function(dt) { // Press Z to thrust if(this.app.keyboard.isPressed(pc.KEY_Z)) { //Move in the direction its facing var force = this.entity.forward.clone().scale(this.speed); this.entity.rigidbody.applyForce(force); } // Rotate up/down/left/right if(this.app.keyboard.isPressed(pc.KEY_UP)){ var force_up = this.entity.right.clone().scale(1); this.entity.rigidbody.applyTorque(force_up); } if(this.app.keyboard.isPressed(pc.KEY_DOWN)){ var force_down = this.entity.right.clone().scale(-1); this.entity.rigidbody.applyTorque(force_down); } if(this.app.keyboard.isPressed(pc.KEY_RIGHT)){ // Rotate to the right var force_right = this.entity.up.clone().scale(-1); this.entity.rigidbody.applyTorque(force_right); } if(this.app.keyboard.isPressed(pc.KEY_LEFT)){ var force_left = this.entity.up.clone().scale(1); this.entity.rigidbody.applyTorque(force_left); } }; // swap method called for script hot-reloading // inherit your script state here Fly.prototype.swap = function(old) { }; // to learn more about script anatomy, please read: // http://developer.playcanvas.com/en/user-manual/scripting/
Test that everything worked. You should be able to fly with Z to thrust and arrow keys to rotate!
8. Spawning Asteroids
Dynamically creating objects is crucial for almost any type of game. In the demo I created, I'm spawning two kinds of asteroids. The first kind just float around and act as passive obstacles. They respawn when they get too far away to create a consistently dense asteroid field around the player. The second kind spawn from further away and move towards the player (to create a sense of danger even if the player is not moving).
We need three things to spawn our asteroids:
- An AsteroidModel entity from which to clone all other asteroids.
- An AsteroidSpawner script attached to the root object that will act as our factory/cloner.
- An Asteroid script to define the behavior of each asteroid.
Creating an Asteroid Model
Create a new entity out of a model of your choosing. This could be something out of the PlayCanvas store, or something from BlendSwap, or just a basic shape. (If you're using your own models, it's good practice to open it up in Blender first to check the number of faces used and optimize it if necessary.)
Give it an appropriate collision shape and a rigid body component (make sure it's dynamic). Once you're happy with it, uncheck the Enabled box:
When you disable an object like this, it's equivalent to removing it from the world as far as the player is concerned. This is useful for temporarily removing objects, or in our case, for keeping an object with all its properties but not having it appear in-game.
Creating the Asteroid Spawner Script
Create a new script called AsteroidSpawner.js and attach it to the Root object in the hierarchy. (Note that the Root is just a normal object that can have any components attached to it, just like the Camera.)
Now open up the script you just created.
The general way of cloning an entity and adding it to the world via script looks like this:
// Create the clone var newEntity = oldEntity.clone(); // Add it to the root object this.app.root.addChild(newEntity); // Give it a new name, otherwise it also gets oldEntity's name newEntity.name = "ClonedEntity"; // Enable it, assuming oldEntity is set to disabled newEntity.enabled = true;
This is how you would clone an object if you already had an "oldEntity" object. This leaves one question unanswered: How do we access the AsteroidModel we created?
There are two ways to do this. The more flexible way is to create a script attribute that holds which entity to clone, so you could easily swap models without touching the script. (This is exactly how we did the camera lookAt script back in step 7.)
The other way is to use the findByName function. You can call this method on any entity to find any of its children. So we can call it on the root object:
var oldEntity = this.app.root.findByName("AsteroidModel");
And so this will complete our code from above. The full AsteroidSpawner script now looks like this:
var AsteroidSpawner = pc.createScript('asteroidSpawner'); // initialize code called once per entity AsteroidSpawner.prototype.initialize = function() { var oldEntity = this.app.root.findByName("AsteroidModel"); // Create the clone var newEntity = oldEntity.clone(); // Add it to the root object this.app.root.addChild(newEntity); // Give it a new name, otherwise it also gets oldEntity's name newEntity.name = "ClonedEntity"; // Enable it, assuming oldEntity is set to disabled newEntity.enabled = true; // Set its position newEntity.rigidbody.teleport(new pc.Vec3(0,0,1)); }; // update code called every frame AsteroidSpawner.prototype.update = function(dt) { };
Test that this worked by launching and looking to see if your asteroid model exists.
Note: I used newEntity.rigidbody.teleport
instead of newEntity.setPosition
. If an entity has a rigidbody, then the rigidbody will be overriding the entity's position and rotation, so remember to set these properties on the rigidbody and not on the entity itself.
Before you move on, try making it spawn ten or more asteroids around the player, either randomly or in some systematic way (maybe even in a circle?). It would help to put all your spawning code into a function so it would look something like this:
AsteroidSpawner.prototype.initialize = function() { this.spawn(0,0,0); this.spawn(1,0,0); this.spawn(1,1,0); // etc... }; AsteroidSpawner.prototype.spawn = function(x,y,z){ // Spawning code here.. }
Creating the Asteroid Script
You should be comfortable adding new scripts by now. Create a new script (called Asteroid.js) and attach it to the AsteroidModel. Since all of our spawned asteroids are clones, they will all have the same script attached to them.
If we're creating a lot of asteroids, it would be a good idea to make sure they are destroyed when we no longer need them or when they're far enough away. Here's one way you could do this:
Asteroid.prototype.update = function(dt) { // Get the player var player = this.app.root.findByName("Ship"); // Replace "Ship" with whatever your player's name is // Clone the asteroid's position var distance = this.entity.getPosition().clone(); // Subtract the player's position from this asteroid's position distance.sub(player.getPosition()); // Get the length of this vector if(distance.length() > 10){ //Some arbitrary threshold this.entity.destroy(); } };
Debugging Tip: If you want to print anything out, you can always use the browser's console as if this were any normal JavaScript app. So you could do something like console.log(distance.toString());
to print out the distance vector, and it will show up in the console.
Before moving on, check that the asteroid does disappear when you move away from it.
9. Spawning Bullets
Spawning bullets will be roughly the same idea as spawning asteroids, with one new concept: We want to detect when the bullet hits something and remove it. To create our bullet system, we need:
- A bullet model to clone.
- A Shoot.js script for spawning bullets when you press X.
- A Bullet.js script for defining each bullet's behavior.
Creating a Bullet Model
You can use any shape for your bullet. I used a capsule just to have an idea of which direction the bullet was facing. Just like before, create your entity, scale it down, and give it a dynamic rigid body and an appropriate collision box. Give it the name "Bullet" so it will be easy to find.
Once you're done, make sure to disable it (with the Enabled checkbox).
Creating a Shoot Script
Create a new script and attach it to your player ship. This time we're going to use an attribute to get a reference to our bullet entity:
Shoot.attributes.add('bullet', { type: 'entity' });
Go back to the editor and hit "parse" for the new attribute to show up, and select the bullet entity you created.
Now in the update function, we want to:
- Clone it and add it to the world.
- Apply a force in the direction the player is facing.
- Place it in front of the player.
You've already been introduced to all these concepts. You've seen how to clone asteroids, how to apply a force in a direction to make the ship move, and how to position things. I'll leave the implementation of this part as a challenge. (But if you get stuck, you could always go look at how I implemented my own Shoot.js script in my project).
Here are some tips that might save you some headache:
- Use
keyboard.wasPressed
instead ofkeyboard.isPressed
. When detecting when the X key is pressed to fire a shot, the former is a convenient way of making it fire only when you press as opposed to firing as long as the button is held. - Use
rotateLocal
instead of setting an absolute rotation. To make sure the bullet always spawns parallel to the ship, it was a pain to calculate the angles correctly. A much easier way is to simply set the bullet's rotation to the ship's rotation, and then rotate the bullet in its local space by 90 degrees on the X axis.
Creating the Bullet Behavior Script
At this point, your bullets should be spawning, hitting the asteroids, and just ricocheting into empty space. The number of bullets can quickly get overwhelming, and knowing how to detect collision is useful for all sorts of things. (For instance, you might have noticed that you can create objects that only have a collision component but no rigid body. These would act as triggers but would not react physically.)
Create a new script called Bullet and attach it to your Bullet model that gets cloned. PlayCanvas has three types of contact events. We're going to listen for collisionend, which fires when the objects separate (otherwise the bullet would get destroyed before the asteroid has a chance to react).
To listen in on a contact event, type this into your init function:
this.entity.collision.on('collisionend', this.onCollisionEnd, this);
And then create the listener itself:
Bullet.prototype.onCollisionEnd = function(result) { // Destroy the bullet if it hits an asteroid if(result.name == "Asteroid") { this.entity.destroy(); } };
This is where the name you gave to your asteroids when you spawned them becomes relevant. We want the bullet to only be destroyed when it collides with an asteroid. result
is the entity it finished colliding with.
Alternatively, you could remove that check and just have it get destroyed on collision with anything.
It's true that there are no other objects in the world to collide with, but I did have some issues early on with the player triggering the bullet's collision for one frame and it disappearing before it could launch. If you have more complicated collision needs, PlayCanvas does support collision groups and masks, but it isn't very well documented at the time of writing.
10. Adding an FPS Meter
We're essentially done with the game itself at this point. Of course, there are a lot of small polish things I added to the final demo, but there's nothing there you can't do with what you've learned so far.
I wanted to show you how to create an FPS meter (even though PlayCanvas already has a profiler; you can hover over the play button and check the profiler box) because it's a good example of adding a DOM element that's outside the PlayCanvas engine.
We're going to use this slick FPSMeter library. The first thing to do is to head over to the library's website and download the minified production version.
Head back to your PlayCanvas editor, create a new script, and copy over the fpsMeter.min.js code. Attach this script to the root object.
Now that the library has been loaded, create a new script that will initialize and use the library. Call it meter.js, and from the usage example on the library's website, we have:
var Meter = pc.createScript('meter'); Meter.prototype.initialize = function(){ this.meter = new FPSMeter(document.body, { graph: 1, heat: 1 }); }; Meter.prototype.update = function(dt){ this.meter.tick(); };
Add the meter script to the root object as well, and launch. You should see the FPS counter in the top left corner of the screen!
11. Adding Text
Finally, let's add some text in our world. This one is a little involved since there are various ways to do it. If you just want to add static UI, one way to do it is to work with the DOM directly, overlaying your UI elements on top of PlayCanvas's canvas element. Another method is to use SVGs. This post discusses some of these different ways.
Since these are all standard ways of handling text on the web, I've opted instead to look at how to create text that exists within the space of the game world. So think of it as text that would go on a sign in the environment or an object in the game.
The way we do this is by creating a material for each piece of text we want to render. We then create an invisible canvas that we render the text to using the familiar canvas fillText method. Finally, we render the canvas onto the material to have it appear in game.
Note that this method can be used for more than just text. You could dynamically draw textures or do anything a canvas can do.
Create the Text Material
Create a new material and call it something like "TextMaterial". Set its diffuse color to black since our text will be white.
Create a plane entity and attach this material to it.
Create the Text Script
You can find the full text.js script in this gist:
https://gist.github.com/OmarShehata/e016dc219da36726e65cedb4ab9084bd
You can see how it sets up the texture to use the canvas as a source, specifically at the line: this.texture.setSource(this.canvas);
Create this script and attach it to your plane. Note how it creates two attributes: text and font size. This way you could use the same script for any text object in your game.
Launch the simulation and you should see the big "Hello World" text somewhere around. If you don't see it, make sure that a) it has a light source nearby and b) you're looking at the correct side of it. The text won't render if you're looking at it from behind. (It also helps to place a physical object near the plane just to locate it at first.)
12. Publishing
Once you've put together your awesome prototype, you can click on the PlayCanvas icon in the top left corner of the screen and select "Publishing". This is where you can publish new builds to be hosted on PlayCanvas and share them with the world!
Conclusion
That's it for this demo. There's a lot more to explore in PlayCanvas, but hopefully this overview gets you comfortable enough with the basics to start building your own games. It's a really nice engine that I think more people should use. A lot of what has been created with it has been tech demos rather than full games, but there's no reason you can't build and publish something awesome with it.
One feature I didn't really talk about but might have been apparent is that PlayCanvas's editor allows you to update your game in real time. This is true for design, in that you can move things in the editor and they'll update in the launch window if you have it open, as well as for code, with its hot-reloading.
Finally, while the editor is really convenient, anything that you can do with it can be done with pure code. So if you need to use PlayCanvas on a budget, a good reference to use is the examples folder on GitHub. (A good place to start would be this simple example of a spinning cube.)
If anything is confusing at all, please let me know in the comments! Or just if you've built something cool and want to share, or figured out an easier way to do something, I'd love to see!