3D Batting Game on the Web (Three.js [r145])
Using JavaScript and Three.js, I created a 3D batting game playable directly in the web browser.
In this article, we explain the implementation of the bat-swinging motion and collision detection with the ball, covering the basic structure of a baseball game with implementation code included.
Recommended for those who want to try making a game with three.js or learn how a baseball game works.
Press the "Swing Bat" button to hit the ball.
If you connect well with the ball, it becomes a home run.
Press the "View 1", "View 2", "View 3", or "View 4" buttons to change the perspective.
Source Code for the 3D Batting Game (Three.js [r145])
<canvas id="can" style="display:block;width:1000px; height:500px; max-width:calc(100vw - 32px);max-height:calc((100vw - 32px) * 500 / 1000);margin:0;padding:0;cursor:grab;" width="1000" height="500"></canvas>
<div style="display:flex">
<div>
<a onclick="cPos=0;changePos();" class="bt101">View 1</a><br>
<a onclick="cPos=1;changePos();" class="bt101">View 2</a><br>
<a onclick="cPos=2;changePos();" class="bt101">View 3</a><br>
<a onclick="cPos=3;changePos();" class="bt101">View 4</a><br>
</div>
<div>
<a onMousedown="furu()" onTouchStart="furu()" class="bt102">Swing Bat</a>
</div>
</div>
<div class="info" style="font-size:32px;height:1.2em;"></div>
<style>
.bt102{
display:inline-block;
margin:4px;
padding:1.5em 1em;
color:#000;
text-shadow:1px 1px 2px #999;
background:#fff;
border:0px none #000;
text-decoration:none;
box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4);
cursor:pointer;
border-radius:10px 10px 10px 10px;
vertical-align:middle;
user-select: none;
font-size:24px;
}
.bt102:active{
box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4) inset;
}
.bt101{
display:inline-block;
margin:4px;
padding:0.1em 0.5em;
color:#000;
text-shadow:1px 1px 2px #999;
background:#fff;
border:0px none #000;
text-decoration:none;
box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4);
cursor:pointer;
border-radius:10px 10px 10px 10px;
vertical-align:middle;
user-select: none;
font-size:24px;
}
.bt101:active{
box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4) inset;
}
</style>
<script>
var controls;
var hh=0;
var bstep=0;//bat
var gBat;
var scene,camera;
var step=0;//0~15:rotation,15~
var bf={x:0, y:2, z:16};
var v= {x:0, y:1, z:-1};
var cam=[
{x:0, y:2.0, z:66},
{x:0, y:4.0, z:66},
{x:-40, y:4, z:50},
{x:0, y:30, z:120},
];
var tar=[
{x:0, y:1.8, z:50},
{x:0, y:1.6, z:50},
{x:0, y:1.6, z:50},
{x:0, y:1.6, z:20},
];
cPos=1;
var main = function () {
//Get target canvas and retrieve width and height
let can=document.getElementById('can');
let w = parseInt(can.style.width);//get width
let h = parseInt(can.style.height);//get height
//■Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0xccddff);
// ■ Camera
// THREE.PerspectiveCamera( field of view (degrees), aspect ratio, near clipping plane, far clipping plane );
camera = new THREE.PerspectiveCamera(20, w/h, 0.1, 10000);
camera.position.set(0, 1.8, 66);// set camera position
//camera.rotation.set(0,0,0);// set camera angle to (0°,0°,0°)
camera.lookAt(new THREE.Vector3(0, 0, 0));// set camera direction to (0,0,0)
scene.add(camera);// add camera to scene
// ■ Renderer
renderer = new THREE.WebGLRenderer({canvas:can, antialias: true});// enable antialiasing
renderer.setSize(w, h);
// Enable shadow map
renderer.shadowMap.enabled = true;
renderer.setClearColor(0x000000, 1);// set background color
// ■ Light ------------------------------------------------
// Ambient light (light applied to all objects from all directions). Prevents unlit areas from being completely black.
let AmbientLight=new THREE.AmbientLight(0xffffff,0.4);
scene.add( AmbientLight );// add ambient light to scene
// Spotlight (color, intensity, distance, angle, penumbra[0-1], decay[1,1])
let AmbientLight=new THREE.AmbientLight(0xffffff,0.4);
scene.add( AmbientLight );// add ambient light to scene
// Spotlight (color, intensity, distance, angle, penumbra[0-1], decay[1,1])
let spot1=new THREE.SpotLight(0xffffff, 0.6,1000,Math.PI/4, 0.4, 1);
spot1.position.set(0,100,50);
spot1.target.position.set(0,0,50);
spot1.castShadow=true;// enable shadows
spot1.shadow.mapSize.width = 4096; // high quality shadows
spot1.shadow.mapSize.height = 4096; // high quality shadows
spot1.shadow.radius=4; // soften shadows (when renderer.shadowMap.type=THREE.PCFShadowMap)
scene.add(spot1);
scene.add(spot1.target);
// Spotlight (color, intensity, distance, angle, penumbra[0-1], decay[1,1])
let spot2=new THREE.SpotLight(0xffffff, 0.6,1000,Math.PI/4, 0.4, 1);
spot2.position.set(0,100,-50);
spot2.target.position.set(0,0,-50);
spot2.castShadow=true;// enable shadows
spot2.shadow.mapSize.width = 4096; // high quality shadows
spot2.shadow.mapSize.height = 4096; // high quality shadows
spot2.shadow.radius=4; // soften shadows (when renderer.shadowMap.type=THREE.PCFShadowMap)
scene.add(spot2);
scene.add(spot2.target);
// Ground
// Load texture
let tx1 = new THREE.TextureLoader().load("./imgs/field.jpg");
let mat1=new THREE.MeshLambertMaterial({
color:0xffffff, map:tx1 ,side:THREE.DoubleSide,
transparent:true, opacity:1.0,
});
let pg=new THREE.PlaneGeometry(140,140,100,100);
let gm=new THREE.Mesh(pg,mat1);
gm.receiveShadow=true;// display shadows cast onto ground
gm.rotation.x=Math.PI/2;
gm.rotation.z=Math.PI;
scene.add(gm);
// ■ Create group
g0=new THREE.Group;
// ■ Create box geometry (width, height, depth)
let boxGeometry=new THREE.BoxGeometry(1,1,1);
let mat2=new THREE.MeshLambertMaterial({
color:0x666666,
side:THREE.FrontSide,
transparent:false, opacity:1.0,
wireframe:false, wireframeLinewidth:0,
});
let mesh11=new THREE.Mesh(boxGeometry,mat2);
mesh11.scale.set(0.2 ,1, 0.2);
mesh11.position.set(-0.2,0.5,0);
mesh11.castShadow=true;
g0.add(mesh11);
let mesh12=new THREE.Mesh(boxGeometry,mat2);
mesh12.scale.set(0.2 ,1, 0.2);
mesh12.position.set( 0.2,0.5,0);
mesh12.castShadow=true;
g0.add(mesh12);
let mesh13=new THREE.Mesh(boxGeometry,mat2);
mesh13.scale.set(0.6 ,1, 0.2);
mesh13.position.set( 0,1.5,0);
mesh13.castShadow=true;
g0.add(mesh13);
let mesh14=new THREE.Mesh(boxGeometry,mat2);
mesh14.scale.set(0.3 ,0.3, 0.3);
mesh14.position.set( 0,2.1,0);
mesh14.castShadow=true;
g0.add(mesh14);
scene.add(g0);
g0.position.set(0.6,0,16);
// Left arm
g1=new THREE.Group;
let mesh15=new THREE.Mesh(boxGeometry,mat2);
mesh15.scale.set(0.2 ,0.8, 0.2);
mesh15.position.set( 0,-0.4,0);
g1.add(mesh15);
g1.position.set(0.9,1.94,16);
g1.rotation.set(0,0,Math.PI/8);
scene.add(g1);
// Right arm
g2=new THREE.Group;
let mesh16=new THREE.Mesh(boxGeometry,mat2);
mesh16.scale.set(0.2 ,0.8, 0.2);
mesh16.position.set( 0.1,-0.4,0);
g2.add(mesh16);
g2.position.set(0,2,0);
g2.rotation.set(0,0,-Math.PI/8);
scene.add(g2);
g2.position.set(0.2,1.94,16);
// ■ Create sphere geometry (radius, horizontal segments, vertical segments, horizontal start angle, vertical start angle)
let sphereGeometry=new THREE.SphereGeometry(0.2,16,16,0,Math.PI*2);
// Create Lambert material (non-glossy material affected by light sources)
let matB=new THREE.MeshLambertMaterial({color:0xffffff, side:THREE.FrontSide, wireframe:false, wireframeLinewidth:1});
mb=new THREE.Mesh(sphereGeometry,matB);
mb.castShadow=true;
mb.position.set(0,-0.8,0);
g3=new THREE.Group;
g3.position.set(bf.x, bf.y, bf.z);
g3.add(mb);
scene.add(g3);
step=0;
// ■ Bat
let matBat=new THREE.MeshLambertMaterial({color:0x880000, side:THREE.FrontSide, wireframe:false, wireframeLinewidth:1});
let points=[
new THREE.Vector2(0.00, 0.00),
new THREE.Vector2(0.08, 0.00),
new THREE.Vector2(0.08, 0.08),
new THREE.Vector2(0.04, 0.08),
new THREE.Vector2(0.08, 1.50),
new THREE.Vector2(0.00, 1.54),
];
// Create lathe (rotational body) geometry by rotating around Y↑ axis (points array, segments, start angle [0], angle [Math.PI*2])
let latheGeometry = new THREE.LatheGeometry(points, 12, 0, Math.PI*2);
// Create mesh from geometry and material
let mBat=new THREE.Mesh(latheGeometry,matBat);
mBat.receiveShadow=true;
mBat.castShadow=true;
gBat=new THREE.Group;
gBat.add(mBat);
gBat.position.set(-1.2,1.45,50);
scene.add(gBat);
bstep=0;
// ■ Batter
g4=new THREE.Group;
let mat3=new THREE.MeshLambertMaterial({
color:0xDDDDDD,
side:THREE.FrontSide,
transparent:false, opacity:1.0,
wireframe:false, wireframeLinewidth:0,
});
let mesh21=new THREE.Mesh(boxGeometry,mat3);
mesh21.scale.set(0.2 ,1, 0.2);
mesh21.position.set(-0.2,0.5,0);
mesh21.castShadow=true;
g4.add(mesh21);
let mesh22=new THREE.Mesh(boxGeometry,mat3);
mesh22.scale.set(0.2 ,1, 0.2);
mesh22.position.set( 0.2,0.5,0);
mesh22.castShadow=true;
g4.add(mesh22);
let mesh23=new THREE.Mesh(boxGeometry,mat3);
mesh23.scale.set(0.6 ,1, 0.2);
mesh23.position.set( 0,1.5,0);
mesh23.castShadow=true;
g4.add(mesh23);
let mesh24=new THREE.Mesh(boxGeometry,mat3);
mesh24.scale.set(0.3 ,0.3, 0.4);
mesh24.position.set( 0,2.1,0);
mesh24.castShadow=true;
g4.add(mesh24);
let mesh25=new THREE.Mesh(boxGeometry,mat3);
mesh25.scale.set(0.2 ,0.8, 0.2);
mesh25.position.set(0.2,1.6, 0.3);
mesh25.rotation.set(-Math.PI*2/6,0,-Math.PI/6);
g4.add(mesh25);
let mesh26=new THREE.Mesh(boxGeometry,mat3);
mesh26.scale.set(0.2 ,0.8, 0.2);
mesh26.position.set(-0.5,1.6,0.3);
mesh26.rotation.set(-Math.PI*2/6,0,-Math.PI/6);
g4.add(mesh26);
scene.add(g4);
g4.position.set(-1.8,0,50);
g4.rotation.set(0,Math.PI/2,0);
// ★
camera.position.set(cam[cPos].x, cam[cPos].y, cam[cPos].z);// set camera position
// Create orbit controls (camera moves with mouse drag, wheel, etc.)
controls = new THREE.OrbitControls(camera, renderer.domElement);
// ★
controls.target.set(tar[cPos].x, tar[cPos].y, tar[cPos].z);
controls.update();
controls.enabled=false;
//renderLoop();
//setInterval(renderLoop,20);
setTimeout(renderLoop,20);
}
function renderLoop () {
// g3.position.z = 50 position
if(hh==0||hh==1){
if(step<100){
// do nothing
}else if(step<120){
g2.rotation.x+=Math.PI/20;
g3.rotation.x+=Math.PI/20;
}else if(step<140){
g2.rotation.x+=Math.PI/20;
g3.position.z+=0.3;
g3.position.y-=0.0115;
}else if(step<=250){
g2.rotation.x=0;
g3.position.z+=0.3;
g3.position.y-=0.0115;
}else if(step<=320){
g2.rotation.x=0;
g3.position.z+=0.3;
g3.position.y-=0.0115;
if(hh==0){
hh=1;
document.querySelector(".info").innerHTML="Strike";
}
}else{
g2.rotation.x=0;
g3.rotation.x=0;
g3.position.x=bf.x;
g3.position.y=bf.y;
g3.position.z=bf.z;
step=0;
hh=0;
document.querySelector(".info").innerHTML="";
}
}else if(hh==2 || hh==3){
v.y-=0.005;
g3.position.x+=v.x;
g3.position.y+=v.y;
g3.position.z+=v.z;
if(g3.position.y<-0.7){
g3.position.y=-0.7;
v.y=-v.y*0.5;
v.z=v.z*0.8;
v.x=v.x*0.8;
}
controls.target.x=g3.position.x;
controls.target.y=g3.position.y;
controls.target.z=g3.position.z;
controls.update();
if(step>300){
hh=0;
step=0;
g2.rotation.x=0;
g3.rotation.x=0;
g3.position.x=bf.x;
g3.position.y=bf.y;
g3.position.z=bf.z;
// ★
controls.target.set(tar[cPos].x, tar[cPos].y, tar[cPos].z);
camera.position.set(cam[cPos].x, cam[cPos].y, cam[cPos].z);// set camera position
controls.update();
document.querySelector(".info").innerHTML="";
}
}
step++;
if(bstep==0){
// reset bat rotation
gBat.rotation.set(0,0,0);
}else{
if(bstep<7){
// bat rotation
gBat.rotation.x=Math.PI/2/6*bstep;
}else if(bstep==7&&(hh!=2&&hh!=3)){
if(g3.position.z>49.7 && g3.position.z<50.3){
hh=3;
document.querySelector(".info").innerHTML="Home Run";
v.x=(g3.position.z-50)/4;
v.y=0.5;
v.z=-0.5;
step=0;
}else if(g3.position.z>48.2 && g3.position.z<51.8){
hh=2;
document.querySelector(".info").innerHTML="Hit";
v.x=(g3.position.z-50)/8;
v.y=0.3;
v.z=-0.4;
step=0;
}else{
hh=1;
document.querySelector(".info").innerHTML="Strike";
}
}
gBat.rotation.z=-Math.PI/2/6*bstep;
bstep++;javascript:furu()
if(bstep>18){bstep=0;}
}
renderer.render( scene, camera );
setTimeout(renderLoop,20);
}
window.addEventListener( 'DOMContentLoaded', main, false );
function furu(){
if(bstep==0){
bstep=1;
}
}
// Switch viewpoint
function changePos(){
controls.target.set(tar[cPos].x, tar[cPos].y, tar[cPos].z);
camera.position.set(cam[cPos].x, cam[cPos].y, cam[cPos].z);// set camera position
controls.update();
}
</script>
