Three.jsでPOLAR STAR(ポーラースター)のようなゲームを作る(1)の続きです。前回定義したクラスを用いてゲームを完成させます。
Contents
ページが読み込まれたときの処理
ページが読み込まれたときにおこなわれる処理を示します。
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 |
window.onload = () => { getGeometriesMaterials(); // スプライトを生成するときに必要なマテリアルの生成(既出) $hideCtrlButtons.checked = false; // [スマホで操作するボタンを非表示にする] は off // canvas のサイズの設定 $main_canvas.width = MAIN_WIDTH; $main_canvas.height = MAIN_HEIGHT; $radar_canvas.width = RADAR_WIDTH; $radar_canvas.height = RADAR_HEIGHT; // Three.js 関連(レンダラーの設定) renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(MAIN_WIDTH, MAIN_HEIGHT); // (カメラの向き調整) camera.position.set(INIT_PLAYER_X, 100, INIT_PLAYER_Y + 100); camera.lookAt(new THREE.Vector3(INIT_PLAYER_X, 40, INIT_PLAYER_Y)); // (ライトの追加) scene.add(light); // (フィールド上に描画される縦横の直線を追加:後述) addLineTop(); for(let i=-1; i<8; i++) addLineNS(25 * i); for(let i=1; i <= 20; i++) addLineEW(20 * i); addEventListeners(); // イベントリスナの追加 player = new Player(); // 自機と敵要塞オブジェクトの生成 boss = new Boss(); draw(); // レンダリングとレーダーの描画 // requestAnimationFrame関数で任意の FPS(60 FPS)で更新処理を繰り返すようにする const INTERVAL = 1000 / 60; let nextUpdateTime = new Date().getTime() + INTERVAL; frameProc(); function frameProc(){ const curTime = new Date().getTime(); if(nextUpdateTime < curTime){ nextUpdateTime += INTERVAL; update(); } requestAnimationFrame(() => frameProc()); } // レンジスライダーでボリューム調整ができるようにする(後述) initVolume('volume-ctrl', sounds); } |
sceneに直線を追加
sceneに縦横の直線を追加する処理を示します。
自機が前進しているように見せかけるため水平な直線を移動できるようにします(実際にはカメラは初期位置から移動しない。背景に相当する水平な直線を移動させている)。水平な直線の3Dオブジェクトを生成したら配列 lineEWs に格納しておきます。
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 |
function addLineTop() { // 動かない直線 const material = new THREE.LineBasicMaterial({ color: 0x008000 }); const geometry = new THREE.Geometry(); geometry.vertices.push(new THREE.Vector3(-100, -2, 20), new THREE.Vector3(1000, -2, 20)); const line = new THREE.Line(geometry, material); scene.add(line); } function addLineEW(z) { // 動く直線 const material = new THREE.LineBasicMaterial({ color: 0x008000 }); const geometry = new THREE.Geometry(); geometry.vertices.push(new THREE.Vector3(-100, -2, 0), new THREE.Vector3(1000, -2, 0)); const line = new THREE.Line(geometry, material); line.position.z = z; scene.add(line); lineEWs.push(line); } function addLineNS(x) { // 動かない直線 const material = new THREE.LineBasicMaterial({ color: 0x008000 }); const geometry = new THREE.Geometry(); geometry.vertices.push(new THREE.Vector3(x, -2, 20), new THREE.Vector3(x, -2, 220)); const line = new THREE.Line(geometry, material); scene.add(line); } |
イベントリスナの追加
イベントリスナを追加する処理を示します。移動用のボタンやキーが押下されたり離されたら該当するフラグのセットまたはクリアをして更新処理時に自機の移動がおこなわれるようにします。またスマホだと弾丸発射ボタンを連打するのは難しいので、pressShotButtonフラグをセットすることで押下したままなら連射できるようにしています。
また[スマホ用操作用ボタンを非表示] の状態が変化したときは、プレイ中であり[非表示]にチェックがされていないときだけ操作用のボタンを表示させるようにしています。
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 64 65 66 67 |
function addEventListeners(){ // ゲーム開始(gameStart関数は後述) document.getElementById('start')?.addEventListener('click', () => gameStart()); // 操作ボタン(主にスマホ用 PCでも使用可)の押下時 const arr1 = ['mousedown', 'touchstart']; for(let i=0; i<arr1.length; i++){ document.getElementById('up')?.addEventListener(arr1[i], () => pressUpKey = true); document.getElementById('down')?.addEventListener(arr1[i], () => pressDownKey = true); document.getElementById('left')?.addEventListener(arr1[i], () => pressLeftKey = true); document.getElementById('right')?.addEventListener(arr1[i], () => pressRightKey = true); document.getElementById('shot')?.addEventListener(arr1[i], () => { pressShotButton = true; player.Shot(); }); } // 操作ボタン押下の解除時 const arr2 = ['mouseup', 'touchend']; for(let i=0; i<arr2.length; i++){ document.getElementById('up')?.addEventListener(arr2[i], () => pressUpKey = false); document.getElementById('down')?.addEventListener(arr2[i], () => pressDownKey = false); document.getElementById('left')?.addEventListener(arr2[i], () => pressLeftKey = false); document.getElementById('right')?.addEventListener(arr2[i], () => pressRightKey = false); document.getElementById('shot')?.addEventListener(arr2[i], () => pressShotButton = false); } // スマホで操作ボタンを操作したときにデフォルトの動作を抑止する const arr3 = ['up', 'down', 'left', 'right', 'shot']; for(let i=0; i<arr3.length; i++){ document.getElementById(arr3[i])?.addEventListener('touchstart', (ev) => ev.preventDefault()); document.getElementById(arr3[i])?.addEventListener('touchend', (ev) => ev.preventDefault()); } // PCのキー操作 document.onkeydown = (ev) => { if(preventDefault && ev.keyCode >= 32 && ev.keyCode <= 40) ev.preventDefault(); if(ev.code == 'ArrowLeft') pressLeftKey = true; if(ev.code == 'ArrowUp') pressUpKey = true; if(ev.code == 'ArrowRight') pressRightKey = true; if(ev.code == 'ArrowDown') pressDownKey = true; if(ev.code == 'Space') player.Shot(); } document.onkeyup = (ev) => { if(ev.code == 'ArrowLeft') pressLeftKey = false; if(ev.code == 'ArrowUp') pressUpKey = false; if(ev.code == 'ArrowRight') pressRightKey = false; if(ev.code == 'ArrowDown') pressDownKey = false; } // [スマホ用操作用ボタンを非表示] の状態が変化したとき $hideCtrlButtons?.addEventListener('change', () => { const display = (!$hideCtrlButtons.checked && isPlaying) ? 'block' : 'none'; $ctrlButtons.style.display = display; }); } |
レンジスライダーでボリューム調整できるようにする
レンジスライダーでボリューム調整できるようにする処理を示します。ここでは elementId で指定した divタグのなかにスライダーや音量を示す値、音量テスト用のボタンを追加する処理をおこなっています(コピペで他のゲームでも使えるはず)。
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 |
function initVolume(elementId, sounds){ let volume = 0.3; const $element = document.getElementById(elementId); const $div = document.createElement('div'); const $span1 = document.createElement('span'); $span1.innerHTML = '音量:'; $div?.appendChild($span1); const $range = document.createElement('input'); $range.type = 'range'; $div?.appendChild($range); const $span2 = document.createElement('span'); $div?.appendChild($span2); $range.addEventListener('input', () => { const value = $range.value; $span2.innerText = value; volume = value / 100; setVolume(); }); setVolume(); $span2.innerText = volume * 100; $span2.style.marginLeft = '16px'; $range.value = volume * 100; $range.style.width = '250px'; $range.style.verticalAlign = 'middle'; $element?.appendChild($div); const $button = document.createElement('button'); $button.innerHTML = '音量テスト'; $button.style.width = '120px'; $button.style.height = '45px'; $button.style.marginTop = '12px'; $button.style.marginLeft = '32px'; $button.addEventListener('click', () => { sounds[0].currentTime = 0; sounds[0].play(); }); $element?.appendChild($button); function setVolume(){ for(let i = 0; i < sounds.length; i++) sounds[i].volume = volume; } } |
ゲーム開始時の処理
ゲーム開始時の処理を示します。プレイが開始されるので
isPlayingをtrueに、isGameoveredをfalseに変更します。また矢印キーを押下したときのデフォルトの動作を抑制するためにpreventDefaultフラグをtrueにします。
スコア、ステージ、残機数をリセットします。前のプレイでのオブジェクトや3Dオブジェクトが残っている場合があるので、これらをすべてクリアします。要塞の描画位置を初期化します。BGMタイプ1の再生を開始するとともにチェックボックスの状態を調べて必要であればスマホ用操作ボタンを表示させ、スタートボタン等を非表示にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function gameStart(){ isPlaying = true; isGameovered = false; preventDefault = true; score = 0; stage = 1; rest = initRest; allClear(); boss.Init(stage); bgm1.currentTime = 0; bgm1.play(); if(!$hideCtrlButtons.checked) $ctrlButtons.style.display = 'block'; $startButtons.style.display = 'none'; } |
更新処理と当たり判定
更新処理を示します。
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
function update(){ if(!isPlaying) return; updateCount++; player.Move(); // 自機の移動 // 敵の移動と弾丸の発射 for(let i = 0; i < enemies.length; i++){ enemies[i].Move(); if(enemies[i].UpdateCount % 180 == 0) enemyBullets.push(new EnemyBullet(enemies[i].X, enemies[i].Y, enemies[i].Y < player.Y)); } // 自機と敵の弾丸の移動 for(let i=0; i<playerBullets.length; i++) playerBullets[i].Move(); for(let i=0; i<enemyBullets.length; i++) enemyBullets[i].Move(); for(let i=0; i<sparks.length; i++) sparks[i].Move(); // 要塞の移動 if(!noCreateEnemy) boss.Move(); // 当たり判定(後述) hitCheck1(); hitCheck2(); hitCheck3(); hitCoreCheck(); // 死亡フラグがセットされたオブジェクトの配列からの除去 enemies = enemies.filter(enemy => !enemy.IsDead); playerBullets = playerBullets.filter(bullet => !bullet.IsDead); enemyBullets = enemyBullets.filter(bullet => !bullet.IsDead); sparks = sparks.filter(spark => !spark.IsDead); // 新しい敵を生成する(敵の個数の上限はステージによる) if(!noCreateEnemy && enemies.length < 3 + stage * 2){ if(updateCount % (60 * 2.5) == 0) enemies.push(new Enemy(true)); if(updateCount % (60 * 5) == 0) enemies.push(new Enemy(false)); } // フィールド上の水平線の移動処理。手前まで来たら初期位置に戻しエンドレスで移動させる。 for(let i=0; i<lineEWs.length; i++){ let z = lineEWs[i].position.z; z += 0.5; if(z > 160){ const a = z - 160; z = a + 20; } lineEWs[i].position.z = z; } // 自機が要塞のコアを射程に捉えたらエネルギーのチャージを開始し、再生されているBGMを変更する // チャージ中はchargedの値を最大 100 まで増やす if(!isGameovered){ if(boss.GetBossHeight() > 0){ if(!charging){ charging = true; bgm1.pause(); bgm1.currentTime = 0; setTimeout(() => { // 自機死亡のタイミングで再生が開始されるかもしれないので自機生存の確認をする if(!player.IsDead){ bgm2.currentTime = 0; bgm2.play(); } }, 1000); } else { charged += 0.1; if(charged > 100){ charged = 100; } } } // BGMを最後のほうまで再生したら最初に巻き戻して再生されるようにする if(bgm1.currentTime > 60 + 18) bgm1.currentTime = 0; if(bgm2.currentTime > 60 + 9) bgm2.currentTime = 0; } // 最後にレーダーとスコアボードへの描画処理をおこなう(後述) draw(); } |
当たり判定
自機が発射した弾丸と敵との当たり判定の処理を示します。衝突したオブジェクトは死亡フラグをセットしてsceneから取り除きます。
1 2 3 4 5 6 7 8 9 10 11 |
function hitCheck1(){ const d = CHARCTER_RADIUS + 3; // 敵の半径 + 弾丸の半径 + 1 for(let i=0; i<playerBullets.length; i++){ const hits = enemies.filter(enemy => Math.pow(enemy.X - playerBullets[i].X, 2) + Math.pow(enemy.Y - playerBullets[i].Y, 2) < d * d); if(hits.length > 0){ hits[0].Dead(); playerBullets[i].Dead(); onHit(hits[0].X, hits[0].Y); // 命中時の処理(加点と爆発の発生など:後述) } } } |
敵が発射した弾丸と自機との当たり判定の処理を示します。
1 2 3 4 5 6 7 8 9 10 |
function hitCheck2(){ if(player.IsDead) return; const d = CHARCTER_RADIUS + 2; // 敵の半径 + 弾丸の半径 const hits = enemyBullets.filter(bullet => Math.pow(bullet.X - player.X, 2) + Math.pow(bullet.Y - player.Y, 2) < d * d); if(hits.length > 0){ hits[0].Dead(); onDead(); // 自機死亡時の処理(後述) } } |
敵本体と自機との当たり判定の処理を示します。
1 2 3 4 5 6 7 8 9 10 |
function hitCheck3(){ if(player.IsDead) return; const d = CHARCTER_RADIUS + CHARCTER_RADIUS; // 敵そのものとの衝突 const hits = enemies.filter(enemy => Math.pow(enemy.X - player.X, 2) + Math.pow(enemy.Y - player.Y, 2) < d * d); if(hits.length > 0){ hits[0].Dead(); onDead(); // 自機死亡時の処理(後述) } } |
自機が発射した弾丸と敵要塞のコアとの当たり判定の処理を示します。
1 2 3 4 5 6 7 8 9 10 |
function hitCoreCheck(){ if(boss.GetCoreHeight() < -100 || playerBullets.length != 1) return; const bullet = playerBullets[0]; if(bullet.Y < 5 && Math.abs(bullet.X - boss.GetCoreX()) <= 4){ bullet.Dead(); onHitCore(); // コアに命中したときの処理(後述) } } |
命中時の処理
ザコ敵に弾丸が命中したときの処理を示します。スコアを加算して効果音を鳴らし火花を生成しています。
1 2 3 4 5 6 7 8 9 10 |
function onHit(x, y){ score += 30; soundHit.currentTime = 0; soundHit.play(); for(let i=0; i<16; i++){ const speed = Math.random() + 0.5; const angle = Math.random() * Math.PI * 2; sparks.push(new Spark(x, 0, y, speed * Math.cos(angle), speed * Math.sin(angle), 0, 0)); } } |
要塞のコアに弾丸が命中したときの処理を示します。スコアを加算して効果音を鳴らし要塞を大爆発させたあと、周囲のザコ敵もいっしょに爆発させています。
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 |
async function onHitCore(){ score += 3000; soundHitCore.currentTime = 0; soundHitCore.play(); bgm2.pause(); // 対ボス戦のBGMを停止する await boss.Explode(); enemiesAllDead(); // つぎのステージに移行するが、5秒間は敵が出現しない時間帯をつくる noCreateEnemy = true; setTimeout(() => noCreateEnemy = false, 5000); // 2秒待機してから通常BGMを鳴らす // 敵要塞を破壊したタイミングでゲームオーバーになるかもしれないので確認している await new Promise(resolve => setTimeout(resolve, 2000)); if(!isGameovered){ bgm1.currentTime = 0; bgm1.play(); } // ステージナンバーをインクリメントして要塞を初期位置にセットする stage++; boss.Init(stage); } |
周囲のザコ敵もいっしょに爆発させる処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function enemiesAllDead(){ charging = false; charged = 0; // すべての弾丸に死亡フラグをセットしてsceneから取り除く for(let i=0; i<playerBullets.length; i++) playerBullets[i].Dead(); for(let i=0; i<enemyBullets.length; i++) enemyBullets[i].Dead(); // すべての敵に死亡フラグをセットしてsceneから取り除くとともに爆発を発生させる for(let i=0; i<enemies.length; i++){ enemies[i].Dead(); for(let k=0; k<16; k++){ const speed = Math.random() + 0.5; const angle = Math.random() * Math.PI * 2; sparks.push(new Spark(enemies[i].X, 0, enemies[i].Y, speed * Math.cos(angle), speed * Math.sin(angle), 0, 0)); } } playerBullets = []; enemies = []; enemyBullets = []; } |
自機死亡時の処理
自機死亡時の処理を示します。
残基を減らして 0 になっていないなら要塞を少し遠ざけてプレイを再開し、0 になったらゲームオーバーの処理をおこなっています。そのさい対ボス戦のBGMが再生されている場合は停止します。理由は自機の再出発点はコアの射程圏外となるからです。
残機が存在する場合は自機死亡から3秒後に自機を復活させますが、このときにフィールド上に存在する敵や弾丸はすべて取り除きます。また復活時は要塞が現在位置から50遠ざかった位置となります。
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 onDead(){ soundDead.currentTime = 0; soundDead.play(); // 火花を発生させる for(let i = 0; i < 24; i++){ const speed = Math.random() + 0.5; const angle = Math.random() * Math.PI * 2; sparks.push(new Spark(player.X, 0, player.Y, speed * Math.cos(angle), speed * Math.sin(angle), 0, 2)); } // 自機死亡フラグのセットと残機マイナス1 の処理 player.IsDead = true; rest--; // 対ボス戦のBGMが再生されている場合は停止する(自機の再出発点はコアの射程圏外となるので) if(bgm2.currentTime > 0){ bgm2.pause(); bgm2.currentTime = 0; } if(rest > 0){ setTimeout(() => { if(bgm1.currentTime == 0) bgm1.play(); boss.KeepAway(50); allClear(); // 敵と弾丸をすべて取り除く(後述) }, 3000); } else onGameover(); // 残機が 0 ならゲームオーバーの処理(後述) } |
sceneと配列から敵と弾丸をすべて取り除き、自機を初期位置に戻す処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function allClear(){ charging = false; charged = 0; for(let i=0; i<playerBullets.length; i++) playerBullets[i].Dead(); for(let i=0; i<enemies.length; i++) enemies[i].Dead(); for(let i=0; i<enemyBullets.length; i++) enemyBullets[i].Dead(); for(let i=0; i<sparks.length; i++) sparks[i].Dead(); playerBullets = []; enemies = []; enemyBullets = []; sparks = []; player.Init(); } |
描画と3Dオブジェクトのレンダリング
レーダーへの描画と3Dオブジェクトのレンダリングの処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function draw(){ renderer.render(scene, camera); // レンダリング radar_ctx.fillStyle = '#00f'; radar_ctx?.fillRect(0, 0, RADAR_WIDTH, RADAR_HEIGHT); player.Draw(); for(let i=0; i<enemies.length; i++) enemies[i].Draw(); showScore(); // スコアの描画(後述) } |
画面上部にスコア等を描画する処理を示します。
要塞との距離を示す部分は距離が離れているときは距離を、コアを射程圏内におさめているときはチャージされたパーセンテージや発射準備が完了した旨を示す文字列を表示しています。あとはDOM要素を取得して変数と文字列を書き込んでいるだけです。
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 |
function showScore(){ const $score = document.getElementById('score'); const $rest = document.getElementById('rest'); const $stage = document.getElementById('stage'); const $distance = document.getElementById('distance'); $score.innerHTML = `Score ${score}`; $rest.innerHTML = `残 ${rest}`; $stage.innerHTML = `Stage ${stage}`; $distance.innerHTML = ``; if(!noCreateEnemy && isPlaying){ if(boss.GetBossHeight() > 0){ if(charging){ if(charged < 100) $distance.innerHTML = `エネルギー充填中 ${Math.floor(charged)} %`; else { if(updateCount % 32 < 16) $distance.innerHTML = `発射準備完了! 敵要塞を破壊せよ!`; else $distance.innerHTML = ``; } } } else $distance.innerHTML = `Distance ${Math.floor(-boss.GetBossHeight() * 10)}`; } } |
ゲームオーバー処理
ゲームオーバー処理を示します。BGMを停止してスマホ操作用ボタンを非表示にします。3秒待機したあとゲームオーバーの効果音を鳴らしてゲーム再開用のボタンを再表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function onGameover(){ if(!isGameovered){ isGameovered = true; sendData(); } bgm1.pause(); bgm2.pause(); $ctrlButtons.style.display = 'none'; setTimeout(() => { $startButtons.style.display = 'block'; isPlaying = false; preventDefault = false; soundGameover.currentTime = 0; soundGameover.play(); }, 3000); } |
スコアランキングを記録する処理を示します。プレイヤー名とスコアをPOSTします。
1 2 3 4 5 6 7 8 9 10 11 12 |
function sendData() { const phpurl = './save-data.php'; let name = $playerName.value; if(name == '') name = '名無しさん'; $.post(phpurl, { name:name, score:score, }); } |
POSTされたデータは以下の処理でcsvファイルに保存します。
save-data.php
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
<?php $maxCount = 100; // 上位 $maxCount 位まで保存 saveRanking($maxCount); function GetFileName(){ $filename = "../p-star-highscore.csv"; return $filename; } function sortByKey($key_name, $sort_order, $array) { foreach ($array as $key => $value) $standard_key_array[$key] = $value[$key_name]; array_multisort($standard_key_array, $sort_order, $array); return $array; } function saveRanking($maxCount){ if(!isset($_POST["name"]) || !isset($_POST["score"])) return; $stack = array(); // csvファイルが存在するならデータを配列に変換 if(file_exists(GetFileName())){ $allData = file_get_contents(GetFileName()); $lines = explode("\n", $allData); foreach ( $lines as $line ) { $words = explode(",", $line); if($words[0] == "") continue; $newArray = array( 'name'=> $words[0], 'score'=> $words[1], 'now'=> $words[2], ); $stack[] = $newArray; } } // 配列に送られてきたデータを追加 $name = ""; $score = 0; $name = $_POST["name"]; $score = $_POST["score"]; $now = date("Y-m-d H:i:s"); // 不適切なデータは処理しない(nameがない、長すぎるなど) if(mb_strlen($name) > 16 || mb_strlen($score) > 16 || $name == "") return; // スコアに数字以外のものがある場合は処理しない if(ctype_digit($score) === false) return; // csvで保存するのでプレイヤー名内にカンマがあったら _ に置換する $name = str_replace(',', '_', $name); $newArray = array( 'name'=> $name, 'score'=> $score, 'now'=> $now, ); $stack[] = $newArray; // socreが大きい順に配列をソート $sorted_array = sortByKey('score', SORT_DESC, $stack); // 上位から $maxCount だけデータを取得してcsvファイルとして保存する $dataCount = count($sorted_array); $str = ""; for($i = 0; $i < $dataCount; $i++){ if($i >= $maxCount) break; $str .= join(",", $sorted_array[$i]); $str .= "\n"; } file_put_contents( GetFileName(), $str, LOCK_EX ); } |