Webサイトで3Dバッティングゲーム(Three.js[r145])
JavaScriptとThree.jsを使って、Webブラウザ上で遊べる3Dバッティングゲームを作ってみました。
本記事では、バットを振る動作の実装やボールとの当たり判定など、野球ゲームの基本構成を実装コード付きで解説します。
「three.jsでゲームを作ってみたい」「野球ゲームの仕組みを学びたい」という方におすすめの内容です。
「バットを振る」ボタンを押してボールを打ちます。
上手にボールに当てるとホームランになります。
「視点1」、「視点2」、「視点3」、「視点4」ボタンを押すと、視点を変えることができます。
3Dバッティングゲームのソースコード(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">視点1</a><br>
<a onclick="cPos=1;changePos();" class="bt101">視点2</a><br>
<a onclick="cPos=2;changePos();" class="bt101">視点3</a><br>
<a onclick="cPos=3;changePos();" class="bt101">視点4</a><br>
</div>
<div>
<a onMousedown="furu()" onTouchStart="furu()" class="bt102">バットを振る</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:回転,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 () {
//描画先canvasを取得して幅と高さ取得
let can=document.getElementById('can');
let w = parseInt(can.style.width);//幅取得
let h = parseInt(can.style.height);//高さ取得
//■シーン
scene = new THREE.Scene();
scene.background = new THREE.Color(0xccddff);
//■カメラ
//THREE.PerspectiveCamera( 画角(度), アスペクト比, カメラに写る最短距離, カメラに写る最長距離 );
camera = new THREE.PerspectiveCamera(20, w/h, 0.1, 10000);
camera.position.set(0, 1.8, 66);//カメラの位置を設定
//camera.rotation.set(0,0,0);//カメラの角度を(0度,0度,0度)にする
camera.lookAt(new THREE.Vector3(0, 0, 0));//カメラの向きを(0,0,0)座標にする
scene.add(camera);//カメラをシーンに追加
//■レンダラー
renderer = new THREE.WebGLRenderer({canvas:can, antialias: true});//アンチエイリアス有効
renderer.setSize(w, h);
//シャドー(影)マップを有効にする
renderer.shadowMap.enabled = true;
renderer.setClearColor(0x000000, 1);//背景色を設定
//■光源------------------------------------------------
//環境光(すべての物体にすべての方向から与える光)光の当たらない部分でも真っ黒にならない
let AmbientLight=new THREE.AmbientLight(0xffffff,0.4);
scene.add( AmbientLight );//環境光をシーンに追加
//スポットライト(色,強さ,距離,角度(angle),半影[0-1],減衰[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;//影を表示可能に
spot1.shadow.mapSize.width = 4096; //影をきれいにする
spot1.shadow.mapSize.height = 4096; //影をきれいにする
spot1.shadow.radius=4; //影をぼかす(renderer.shadowMap.type=THREE.PCFShadowMapの時)
scene.add(spot1);
scene.add(spot1.target);
//スポットライト(色,強さ,距離,角度(angle),半影[0-1],減衰[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;//影を表示可能に
spot2.shadow.mapSize.width = 4096; //影をきれいにする
spot2.shadow.mapSize.height = 4096; //影をきれいにする
spot2.shadow.radius=4; //影をぼかす(renderer.shadowMap.type=THREE.PCFShadowMapの時)
scene.add(spot2);
scene.add(spot2.target);
//地面
//テクスチャのロード
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;//落ちてきた影を表示する
gm.rotation.x=Math.PI/2;
gm.rotation.z=Math.PI;
scene.add(gm);
//■グループの作成
g0=new THREE.Group;
//■直方体ジオメトリの作成(幅、高さ、奥行き)
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);
//左腕
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);
//右腕
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);
//■球体ジオメトリの作成(半径, 水平分割数幅, 垂直分割数, 水平方向開始角度, 垂直方向開始角度)
let sphereGeometry=new THREE.SphereGeometry(0.2,16,16,0,Math.PI*2);
//ランバートシェーディング材質(光源の影響を受ける光沢の無い材質)の作成
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;
//■バット
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),
];
//Y↑軸で回転させて旋盤(回転体)ジオメトリ作成(点配列,分割数,開始角[0],角度[Math.PI*2])
let latheGeometry = new THREE.LatheGeometry(points, 12, 0, Math.PI*2);
//ジオメトリとマテリアルからメッシュを作成
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;
//■バッター
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);//カメラの位置を設定
//オービットコントロールの作成(マウスの左右ボタンドラッグ、ホイール等でカメラが動くようになる)
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の位置
if(hh==0||hh==1){
if(step<100){
//何もしない
}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="ストライク";
}
}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);//カメラの位置を設定
controls.update();
document.querySelector(".info").innerHTML="";
}
}
step++;
if(bstep==0){
//バットの回転を元に戻す
gBat.rotation.set(0,0,0);
}else{
if(bstep<7){
//バットの回転
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="ホームラン";
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="ヒット";
v.x=(g3.position.z-50)/8;
v.y=0.3;
v.z=-0.4;
step=0;
}else{
hh=1;
document.querySelector(".info").innerHTML="ストライク";
}
}
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;
}
}
//視点の切り替え
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);//カメラの位置を設定
controls.update();
}
</script>
