Over a million developers have joined DZone.

Developing a Street Basketball Game (Part 1)

Some days ago, I released a new version of the WhitestormJS framework and decided to develop a game using almost all of its functionality. The task was to make a 3D game that requires complex 3D physics like concave objects and softbodies. Follow along with this tutorial and see what I've got thus far.

· Web Dev Zone

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

Creating a 3D Browser Game With Physics in a Few Steps...

Some days ago, I released a new version of the WhitestormJS framework named r10 and decided to develop a game using almost all of its functionality. (Just was curious as to how easy it would be.) The task was to make a 3D game that requires complex 3D physics like concave objects and softbodies.

Setting up the Workflow

Before starting to develop the game, I added a couple of directories that Ii will use later: js, css, textures, and img. The difference between the textures and img folders is that the textures folder will be used for images that will be used in the 3D part of the app and img is for images that are used in HTML, CSS, and stuff like that.

Rollup

This is a useful thing when you need to work with ES6 models in front-end, especially when you use most of ES6 features and want them on old browser versions. I used it with two plugins: babel for rollup and uglify for rollup to develop a cross-browser 3D app.

Bower

Some libraries are getting updates regularly (yep, like whitestorm.js). For them, I need a front-end package manager like Bower. (You can use npm too, but Bower will be enough.) For tweening, I used the GSAP library, because of it’s performance—and also we'll need basket.js in order to keep such a large library like WhitestormJS in localStorage.

…And also, I wrote a small Bower plugin that will help with preloading. Check it out here.

bower install whitestormjs gsap progress-loader basket.js


My index.html file will be simple and contain just loaded scripts and style.css for featured usage. Note that we don’t load whitestorm.js yet. We will add it using basket.js.

<!DOCTYPE html>
<html>
  <head>
    <title>ThrowIntoBasket v1.0</title>
    <meta name="viewport" content="width=device-width, user-scalable=no">
    <link rel="stylesheet" type="text/css" href="css/style.css">
  </head>
  <body>

  <!-- Nothing there yet -->

  </body>
  <script type="text/javascript" src="bower_components/rsvp/rsvp.min.js"></script>
  <script type="text/javascript" src="bower_components/basket.js/dist/basket.js"></script>
  <script type="text/javascript" src="bower_components/gsap/src/minified/TweenLite.min.js"></script>
  <script type="text/javascript" src="bower_components/gsap/src/minified/plugins/CSSPlugin.min.js"></script>
  <script type="text/javascript" src="bower_components/ProgressLoader/progress-loader.js"></script>
  <!-- Main APP -->
  <script type="text/javascript" src="js/build/app.js"></script>
</html>


App.js

As you already noticed, we have app.js included. So, let’s create an empty 3D world with enabled softbody physics (which will be used later for a net), auto-resize, filled background, camera position & aspect, and gravity:

const APP = {
  /* === APP: config === */
  /* APP */
  bgColor: 0xcccccc,
  /* BASKET */
  basketY: 20,

  /* === APP: variables === */
  isMobile: navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/),

  /* === APP: init === */

  init() {
    APP.world = new WHS.World({
      autoresize: "window",
      softbody: true,

      background: {
        color: APP.bgColor
      },

      fog: {
        type: 'regular',
        hex: 0xffffff
      },

      camera: {
        z: 50,
        y: APP.basketY,
        aspect: 45
      },

      gravity: {
        y: -200
      }
    });

    // Add ProgressLoader.
    APP.ProgressLoader = new ProgressLoader(APP.isMobile ? 12 : 14);

    // Lets make camera look at the basket.
    APP.camera = APP.world.getCamera();
    APP.camera.lookAt(new THREE.Vector3(0, APP.basketY, 0));

    APP.createScene(); // 1
    // TODO: Add other init funcs.

    APP.world.start(); // Ready.

    // TODO: Check for loading progress.
  }

  // ...TODO
};

basket.require({ url: 'bower_components/whitestorm/build/whitestorm.js' }).then(() => {
  APP.init();
});

Now we have a basic world that has 0xcccccc (grey) as it’s background color, gravity set as {x: 0, y: -200, z: 0} and camera that will be at same height as our basket. Also, we start the app only after we have whitestorm.js loaded.

I counted 14 objects that we will load for desktop devices. (I hope we’ll also have an optimized version for mobile devices, so I used APP.isMobile variable which is a boolean.)

Creating a Scene. Ground and a Wall.

Next thing we’ll do is implementing a floor with just a ground and wall. Wall and ground will be very similar and we create them both from one object. We’ll create a default plane 1000x800 with buffer geometry (Actually no need for buffer geometry there, but somebody says it increases performance); it won’t be a big deal for a plane with 1x1 segments.

0 mass means that this object is not affected by gravity and won’t move when it collides with other objects. And we need to rotate it by (-Math.PI / 2, 0, 0) euler.

const APP = {
  // APP: config. <-
  // APP: init. <-
  // APP: variables. <-

  createScene() {
    /* GROUND OBJECT */
    APP.ground = new WHS.Plane({
      geometry: {
        buffer: true,
        width: 1000,
        height: 800
      },

      mass: 0,

      material: {
        kind: 'phong',
        color: APP.bgColor
      },

      pos: {
        y: -20,
        z: 120
      },

      rot: {
        x: -Math.PI / 2
      }
    });

    APP.ground.addTo(APP.world).then(() => APP.ProgressLoader.step());

    /* WALL OBJECT */
    APP.wall = APP.ground.clone();

    APP.wall.position.y = 180;
    APP.wall.position.z = -APP.basketDistance;
    APP.wall.rotation.x = 0;
    APP.wall.addTo(APP.world).then(() => APP.ProgressLoader.step());
  }

  // ...TODO
}


When we add each object to world, it returns a promise which is resolved when an object is generated and added to the world. Such stuff is very useful when working with models or objects and you want to know they are ready. This is the case when we need to update loading status as each one is added.

Lights

We need just two simple lights to get a cool effect. Ambient light will cast light on the whole scene and point light will cast only on objects near the basket.

const APP = {
  // APP: config. <-
  // APP: variables. <-
  // APP: init. <-
  // APP: createScene. <-

  addLights() {
    new WHS.PointLight({
      light: {
        distance: 100,
        intensity: 1,
        angle: Math.PI
      },

      shadowmap: {
        width: 1024,
        height: 1024,

        left: -50,
        right: 50,
        top: 50,
        bottom: -50,

        far: 80,

        fov: 90,
      },

      pos: {
        y: 60,
        z: -40
      },
    }).addTo(APP.world).then(() => APP.ProgressLoader.step());

    new WHS.AmbientLight({
      light: {
        intensity: 0.3
      }
    }).addTo(APP.world).then(() => APP.ProgressLoader.step());
  }

  // ...TODO
};


That’s how it looks now:

We have a scene with a wall, ground, and lights.


Adding a Basket

After we've added lights, we need to have a basket in which we will throw a ball. Our basket will be a simple torus with a thin tube radius. Also, I experimented with material parameters to give it a metallic look.

const APP = {
  // APP: config. <-

  /* BALL */
  ballRadius: 6,
  /* BASKET */
  basketColor: 0xff0000,
  getBasketRadius: () => APP.ballRadius + 2,
  basketTubeRadius: 0.5,
  basketY: 20,
  basketDistance: 80,
  getBasketZ: () => APP.getBasketRadius() + APP.basketTubeRadius * 2 - APP.basketDistance,

  // APP: variables. <-
  // APP: init. <-
  // APP: createScene. <-

  addBasket() {
    // TODO: Make a backboard.

    /* BASKET OBJECT */
    APP.basket = new WHS.Torus({
      geometry: {
        buffer: true,
        radius: APP.getBasketRadius(),
        tube: APP.basketTubeRadius,
        radialSegments: 16,
        tubularSegments: APP.isMobile ? 8 : 16
      },

      shadow: {
        cast: false
      },

      mass: 0,

      material: {
        kind: 'standard',
        color: APP.basketColor,
        metalness: 0.8,
        roughness: 0.5,
        emissive: 0xffccff,
        emissiveIntensity: 0.2
      },

      pos: {
        y: APP.basketY,
        z: APP.getBasketZ()
      },

      physics: {
        type: 'concave' // 'convex' by default.
      },

      rot: {
        x: Math.PI / 2
      }
    });

    APP.basket.addTo(APP.world).then(() => APP.ProgressLoader.step());

    // TODO: Make a net.
  }

  // ...TODO
};


One more thing we need to do is set a “concave” physics object type. By default, each mesh with a concave geometry works as a convex mesh for physics, but you can simply make it concave by setting this parameter. We make it static by applying 0 mass to this object.

Adding a Ball

Now it’s time to make a ball. This time, we are going to use optimization for mobile devices again. (Geometry with less vertices. It won’t work with physics because when we process sphere collisions... it’s enough to have just a sphere radius.)

This time, we also have to apply x3 restitution to make our sphere bounce like a basketball ball.

const APP = {
  // APP: config. <-
  // APP: variables. <-
  // APP: createScene. <-
  // APP: addLights. <-
  // APP: addBasket. <-

  addBall() {
    /* BALL OBJECT */
    APP.ball = new WHS.Sphere({
      geometry: {
        buffer: true,
        radius: APP.ballRadius, 
        widthSegments: APP.isMobile ? 16 : 32,
        heightSegments: APP.isMobile ? 16 : 32
      },

      mass: 120,

      material: {
        kind: 'phong',
        map: WHS.texture('textures/ball.png'),
        normalMap: WHS.texture('textures/ball_normal.png'),
        shininess: 20,
        reflectivity: 2,
        normalScale: new THREE.Vector2(0.5, 0.5)
      },

      physics: {
        restitution: 3
      }
    });

    APP.ball.addTo(APP.world).then(() => APP.ProgressLoader.step());
  }

  // ...TODO
};


We use 2 textures for ball material. The first one (in map parameter) is a default colored texture. The second one (normalMap) defines how light will be cast on the ball.

I generated a normal map using this normal map generator. I use it often. You can make a normal map not just from a black-white heightmap. For generating this one, I just used the first texture.

What ball map and normal map look like.


Summary

If everything is done correctly, you should see something like this:

And the ball will fall down…


I hope to have part 2 ready for you guys next week!

Links

A result of this part is also available on GitHub.

Full game: Github | Demo

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
web developement ,javascript ,webgl ,three.js ,3d ,github ,tutorial ,bower ,npm ,game

Published at DZone with permission of Alexander Buzin. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}