これまで線路プレートと目覚まし時計の移動と描画の処理をしてきましたが、今回はゲームとして完成させます。
VIDEO
動作確認は こちらから
Window: load イベント時の処理
ページがロードされたらMain関数を実行します。ここでやっているのはシーンの作成、光源の設定、レンダラーを作成、線路プレートと目覚まし時計を生成してシーンに追加する処理です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
let scene : THREE . Scene ;
let camera : THREE . PerspectiveCamera ;
let renderer : THREE . WebGLRenderer ;
const width = 600 ;
const height = 480 ;
let alarmClock1 : AlarmClock ;
// 移動時とゲームオーバー時に音を鳴らす
let soundMove : HTMLAudioElement = new Audio ( './sounds/move.mp3' ) ;
let soundGameover : HTMLAudioElement = new Audio ( './sounds/gameover.mp3' ) ;
let isGameOver = false ;
// ゲーム開始時刻と経過時間
let startTime ;
let passedTime ;
window . addEventListener ( 'load' , Main ) ;
function Main ( ) {
document . onkeydown = OnKeyDown ;
// シーンを作成
scene = new THREE . Scene ( ) ;
// カメラを作成
camera = new THREE . PerspectiveCamera ( 45 , width / height , 1 , 10000 ) ;
camera . position . set ( -80 , 64 , 0 ) ;
camera . lookAt ( 0 , 2 , 0 ) ;
// 平行光源
const light = new THREE . DirectionalLight ( 0xffffff ) ;
light . intensity = 2 ; // 光の強さを2倍に
light . position . set ( -1 , 1 , 0.5 ) ;
scene . add ( light ) ;
// 環境光
const ambientLight = new THREE . AmbientLight ( 0xffffff , 0.3 ) ;
scene . add ( ambientLight ) ;
// レンダラーを作成
renderer = new THREE . WebGLRenderer ( {
canvas : < HTMLCanvasElement > document . getElementById ( 'can' )
} ) ;
renderer . setPixelRatio ( window . devicePixelRatio ) ;
renderer . setSize ( width , height ) ;
renderer . setClearColor ( 0x006000 ) ;
// 線路プレートを生成してシーンに追加
let startTrackPlate = InitTrackPlates ( ) ;
// 目覚まし時計を生成してシーンに追加
AddClock ( startTrackPlate ) ;
renderer . render ( scene , camera ) ;
setInterval ( Update , 1000 / 60 ) ;
// ゲーム開始!
GameStart ( ) ;
}
線路プレートを生成してシーンに追加する
線路プレートを生成してシーンに追加する処理を示します。
乱数を生成してシーンに追加する線路プレートを決めます。縦移動だけ、横移動だけというプレートが多くなるとゲームがやりにくいのでNSWE型やNESW型、NWSE型が出現しやすくしています。また余裕をもってプレイを開始できるように、目覚まし時計の初期位置は固定し、最初はある程度線路が繋がっている状態にしておきます。InitTrackPlates関数の戻り値は目覚まし時計の初期位置にある線路プレートです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 0 ~ max-1 の整数を生成する
function getRandomInt ( max ) {
return Math . floor ( Math . random ( ) * Math . floor ( max ) ) ;
}
function InitTrackPlates ( ) : TrackPlate {
let start = null ;
for ( let colum = 0 ; colum < columMax ; colum ++) {
for ( let row = 0 ; row < rowMax ; row ++) {
// 最初はある程度線路が繋がっている状態にするため、3箇所はPlateTypeを固定にする
if ( colum == 0 && row == 2)
new TrackPlate(0, 2, PlateType.SE);
else if ( colum == 1 && row == 2)
new TrackPlate(1, 2, PlateType.NWSE);
else if ( colum == 1 && row == 3)
start = new TrackPlate(1, 3, PlateType.NSWE); // 目覚まし時計の初期位置
else if ( colum == 3 && row == 1) {
// 線路プレートが存在しない初期位置
TrackPlate.BlankColum = colum;
TrackPlate . BlankRow = row ;
}
else {
// それ以外は乱数で線路プレートを生成する
let plateType : PlateType = PlateType . NS ;
let r = getRandomInt ( 12 ) ;
if ( r == 0 | | r == 1 )
plateType = PlateType . NSWE ;
if ( r == 2 )
plateType = PlateType . NS ;
if ( r == 3 )
plateType = PlateType . WE ;
if ( r == 4 | | r == 5 )
plateType = PlateType . NESW ;
if ( r == 6 )
plateType = PlateType . NE ;
if ( r == 7 )
plateType = PlateType . SW ;
if ( r == 8 | | r == 9 )
plateType = PlateType . NWSE ;
if ( r == 10 )
plateType = PlateType . NW ;
if ( r == 11 )
plateType = PlateType . SE ;
// 生成されたオブジェクトはTrackPlateクラスのコンストラクタ内でシーンに追加される
new TrackPlate ( colum , row , plateType ) ;
}
}
}
return start ;
}
目覚まし時計を生成してシーンに追加する
次に目覚まし時計を生成してシーンに追加する処理をする関数を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function AddClock ( startTrackPlate : TrackPlate ) {
// モデルデータを読み込む
//@ts-ignore
const loader = new THREE . ColladaLoader ( ) ;
// 3DデータのCOLLADA(.dae)ファイルのパスを指定
loader . load ( './clock.dae' , collada => {
let model ;
model = collada . scene ;
// 線路プレートにあうように大きさとY座標を調整する
model . position . y = 8 ;
model . scale . x = 5.5 ;
model . scale . y = 5.5 ;
model . scale . z = 5.5 ;
// 最初は南向き
model . rotation . z = Math . PI / 2 * 3 ;
// AlarmClockクラスのコンストラクタにモデルデータと開始位置を渡す
alarmClock1 = new AlarmClock ( model , startTrackPlate ) ;
for ( let i = 0 ; i < model . children . length ; i ++) {
// 目覚まし時計の文字盤はpngファイルから生成したテクスチャを貼り付ける
if ( model . children [ i ] . material ! = null ) {
if ( model . children [ i ] . material . name == "material_clock_face" ) {
const loader = new THREE . TextureLoader ( ) ;
let texture = loader . load ( 'face.png' ) ;
const material = new THREE . MeshStandardMaterial ( { map : texture , } ) ;
model . children [ i ] . material = material ;
}
}
}
} ) ;
}
更新処理
setInterval関数によってUpdate関数が1秒間に60回呼び出されます。このときにおこなわれる処理を示します。
現在時刻を取得してゲーム開始時に取得しておいた値(後述)と比較して経過時間を求めます。これをスコア代わりに表示させます(後述)。ゲームオーバー時は時間の計測は停止され、目覚まし時計の移動もされません。
function Update ( ) {
if ( ! isGameOver ) {
alarmClock1 . Move ( ) ;
let now = new Date ( ) ;
let nowTime = now . getTime ( ) ;
passedTime = nowTime - startTime ; // ゲーム開始からの経過時間を求める(単位はミリ秒)
}
ShowTextIfGameOver ( ) ; // ゲームオーバーであれば文字列を表示
ShowTime ( ) ; // 経過時間をスコアとして表示
// レンダリング
renderer . render ( scene , camera ) ;
}
ゲーム開始とゲームオーバー時の処理
ゲームスタートのときはスタート時刻を取得し、ゲームオーバーのときはゲームオーバー時の音を鳴らします。またisGameOverフラグをセットして線路プレートの移動操作ができないようにします。
function GameStart ( ) {
// スタート時刻をリセットする
let now = new Date ( ) ;
startTime = now . getTime ( ) ;
}
function GameOver ( ) {
isGameOver = true ;
soundGameover . currentTime = 0 ;
soundGameover . play ( ) ;
}
// ゲームオーバー時にfunction GameOver()を呼び出せるようにする
class AlarmClock {
GameOver ( ) {
this . DirectOfMove = DirectOfMove . None ;
GameOver ( ) ;
}
}
キーが押されたときの処理
キーが押されたときの処理を示します。ゲームオーバーでなければ線路プレートの移動処理がおこなわれます。実際に移動することができた場合は音を鳴らします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function OnKeyDown ( e : KeyboardEvent ) {
if ( e . keyCode == 37 && !isGameOver) { // 左
if (TrackPlate.MoveWest()) {
soundMove.currentTime = 0;
soundMove . play ( ) ;
}
}
if ( e . keyCode == 39 && !isGameOver) { // 上
if (TrackPlate.MoveEast()) {
soundMove.currentTime = 0;
soundMove . play ( ) ;
}
}
if ( e . keyCode == 38 && !isGameOver) { // 右
if (TrackPlate.MoveNorth()) {
soundMove.currentTime = 0;
soundMove . play ( ) ;
}
}
if ( e . keyCode == 40 && !isGameOver) { // 下
if (TrackPlate.MoveSouth()) {
soundMove.currentTime = 0;
soundMove . play ( ) ;
}
}
if ( e . keyCode == 83 && isGameOver) { // Sキー
Retry(); // ゲームオーバー後の再挑戦
}
}
再挑戦するときの処理
再挑戦するときの処理を示します。線路プレートと目覚まし時計をシーンから取り除き、新しく生成しなおします。
function Retry ( ) {
// 線路プレートをシーンから取り除く。配列 TrackPlate.Platesもクリアする
TrackPlate . Plates . forEach ( plate => scene . remove ( plate . Group ) ) ;
TrackPlate . Plates = [ ] ;
// 目覚まし時計をシーンから取り除く
scene . remove ( alarmClock1 . Model ) ;
// 新しく生成する
let startTrackPlate = InitTrackPlates ( ) ;
AddClock ( startTrackPlate ) ;
// isGameOverフラグをクリアしてゲーム開始
isGameOver = false ;
GameStart ( ) ;
}
文字列を表示させる処理
文字列を表示させる処理を示します。
HTMLは以下のようになっています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
< ! DOCTYPE html >
< html >
< head >
< title > Three . js でチクタクバンバンのようなゲームをつくってみた< /title >
< meta charset ="UTF-8" />
<script src ="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js" > </script>
<script src ="./ColladaLoader.js" > </script>
< /head >
< body >
< canvas id ="can" > < /canvas >
< div id ="time" style ="position: absolute; top: 0; left: 0;" > < /div >
< div id ="gameover" style ="position: absolute; top: 0; left: 0;" > < /div >
< div id ="retry" style ="position: absolute; top: 0; left: 0;" > < /div >
<script src ="TrackPlate.js" > </script>
<script src ="AlarmClock.js" > </script>
<script src ="functions.js" > </script>
<script src ="app.js" > </script>
< /body >
< /html >
document.getElementById関数でドキュメント要素を取得して、そこに文字列を描画します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function ShowTextIfGameOver ( ) {
// テキストフィールドに表示
const tf1 = document . getElementById ( "gameover" ) ;
if ( isGameOver )
tf1 . innerHTML = "GAME OVER" ;
else
tf1 . innerHTML = "" ;
tf1 . style . transform = "translate(160px, 180px)" ;
tf1 . style . color = "white" ;
tf1 . style . fontSize = "48px" ;
const tf2 = document . getElementById ( "retry" ) ;
if ( isGameOver )
tf2 . innerHTML = "Press S key to Retry" ;
else
tf2 . innerHTML = "" ;
tf2 . style . transform = "translate(150px, 250px)" ;
tf2 . style . color = "white" ;
tf2 . style . fontSize = "32px" ;
}
function ShowTime ( ) {
const tf1 = document . getElementById ( "time" ) ;
let minutes = Math . floor ( passedTime / 1000 / 60 ) ; // passedTimeはミリ秒
let str = minutes + " 分 " + ( ( passedTime - 60000 * minutes ) / 1000 ) . toFixed ( 1 ) + " 秒" ;
tf1 . innerHTML = str ;
tf1 . style . transform = "translate(40px, 20px)" ;
tf1 . style . color = "#ffffff" ;
tf1 . style . fontSize = "24px" ;
}
VIDEO
動作確認は こちらから
鳩でも分かるC#管理人からのお願い
できる仕事であれば請け負います。鳩でもわかるC#管理人はクラウドワークスに在宅ワーカーとして登録しています。お仕事の依頼もお待ちしております。
⇒ 仕事を依頼する
コメントについて
コメントで英語などの外国語でコメントをされる方がいますが、管理人は日本語以外はわからないので基本的に内容が理解できず、承認することもありません。それからへんな薬を売っているサイトやリンク先のサイトが存在しないというスパムコメントも多々あります。
Some people make comments in foreign languages such as English, but since the manager does not understand anything other than Japanese, he basically cannot understand the content and does not approve it. Please use Japanese when making comments.
そんななか日本語のコメントもいただけるようになりました。「○○という変数はどこで宣言されているのか?」「××というメソッドはどこにあるのか」「例外が発生する」「いっそのことソース丸ごとくれ」という質問ですが、管理人としては嬉しく思います。「自分が書いた記事は読まれているんだな」と。疑問点には可能な限り答えます。記事に問題があれば修正いたします。
そのうえでお願いがあります。「匿名」という味も素っ気もない名前ではなく、捨てハンでいいのでなにかハンドルネームをつくってほしいと思います。
管理人のモチベーションアップのために
よろしければご支援お願いします。
⇒ 管理人の物乞いリスト