Unityを使わずにフリスビーを犬に届けよ!を作ってみる(4)の続きです。
今回は移動する障害物をつくります。
フリスビーを犬に届けよ!の元ネタ
Unishar-ユニシャー【Unityでのゲーム開発を手助けするメディア】
MovableObstacleクラスの定義
Obstacleクラスを継承してMovableObstacleクラスを定義します。Obstacleクラスについては第一回の記事を参照してください。
コンストラクタの引数が追加されています。最初の2つはX方向とY方向の移動速度です。最後の引数のmaxCountはその回数だけ移動したら逆向きに移動するという意味です。これで同じ場所を往復させることができます。
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 |
namespace Frisbee { public class MovableObstacle : Obstacle { int _update = 0; int _maxCount = 0; double _x = 0; double _y = 0; public MovableObstacle(int centerX, int centerY, int width, int height, double vx, double vy, int maxCount) : base(centerX, centerY, width, height) { _x = centerX; _y = centerY; VX = vx; VY = vy; _maxCount = maxCount; } public double VX { private set; get; } public double VY { private set; get; } public void Update() { if (_update % (_maxCount * 2) < _maxCount) { _x += VX; _y += VY; } else { _x -= VX; _y -= VY; } CenterX = (int)_x; CenterY = (int)_y; _update++; } } } |
FrisbeeGameクラスの修正
障害物は固定されているものと移動するものがあるので、FixedObstaclesプロパティとMovableObstaclesプロパティを定義しました。
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 |
public class FrisbeeGame { public List<Obstacle> FixedObstacles { protected set; get; } public List<MovableObstacle> MovableObstacles { protected set; get; } public int StageNumber { set; get; } public FrisbeeGame() { StageNumber = 1; // 追加 Player = new Player(this); FixedObstacles = new List<Obstacle>(); MovableObstacles = new List<MovableObstacle>(); // 警告を消すためになにか入れておく Start = new Obstacle(0, 0, 0, 0); Goal = new Obstacle(0, 0, 0, 0); Init(); } } |
各ステージを生成するInitメソッドを示します。障害物の生成は後述するCreateObstaclesメソッドでおこないます。
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 |
public class FrisbeeGame { public void Init() { FixedObstacles = new List<Obstacle>(); MovableObstacles = new List<MovableObstacle>(); List<Obstacle> starts = new List<Obstacle>(); List<Obstacle> goals = new List<Obstacle>(); int fieldWidth = 0; CreateObstacles(starts, goals, ref fieldWidth); // スタート地点にある台を描画する位置を決める int startCenterX = (int)starts.Average(o => o.CenterX); int startCenterY = (int)starts.Average(o => o.CenterY); int startWidth = (int)starts.Max(o => o.Right) - (int)starts.Min(o => o.Left); int startHeight = (int)starts.Max(o => o.Top) - (int)starts.Min(o => o.Bottom); Start = new Obstacle(startCenterX, startCenterY, startWidth, startHeight); // ゴール地点に描画する画像の位置を決める int goalCenterX = (int)goals.Average(o => o.CenterX); int goalCenterY = (int)goals.Average(o => o.CenterY); int goalWidth = (int)goals.Max(o => o.Right) - (int)goals.Min(o => o.Left); int goalHeight = (int)goals.Max(o => o.Top) - (int)goals.Min(o => o.Bottom); Goal = new Obstacle(goalCenterX, goalCenterY, goalWidth, goalHeight); // フリスビーの中心の初期y座標はスタート地点にあるブロックの上面のy座標+フリスビーの厚さの半分 Player.SetStartPosition(startCenterX, starts.Max(o => o.Top) + PLAYER_THICKNESS / 2); MaxX = fieldWidth; Player.StageClear(); } } |
各ステージにある障害物の位置はリソースのテキストファイルを読み込んで取得します。ステージの番号ごとに異なるものを読み込みます。
frisbee-stage-1.txt
frisbee-stage-2.txt
frisbee-stage-3.txt
frisbee-stage-4.txt
frisbee-stage-5.txt
読み込んだ文字列がこのようになっていると・・・
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
######################### #・・・・・#・・・・・#・・・・・・・・・・・# #・・・・・▽・・・・・#・・・・・半・・・・・# #・・・・・・・・・・・#・・・・・#・・・・・# #・・・・・・・・・・・#・・・・・#・・・・・# #・・・・・・・・・・・#・・・LL#・・・・・# #・・・・・・・・・・・▼・・・・・#・・・・・# #・・・・・△・・・・・▼・・・・・#・・・・・# #・・・・・#・・・・・・・・・・・#・・・・・# #・・・・・#・・・・・・・・・・・#RR・・・# #・・・・・#・・・・・・・・・・・#・・・・・# #・・・・・#・・・・・・・・・・・#・・・・・# #・・・・・#・・・・・▲・・・・・#・・・・・# #・・・・・#・・・・・▲・・・・・#・GGG・# #・SSS・#・・・・・#・・・・・#・GGG・# ######################### |
#は固定された障害物
SSSはスタート地点
GGGはゴール地点
▽はその位置とそのひとつ下の間を移動する障害物
△はその位置とそのひとつ上の間を移動する障害物
▼はその位置とそのふたつ下の間を移動する障害物
▲はその位置とそのふたつ上の間を移動する障害物
Rはその位置とそのふたつ右の間を移動する障害物
Lはその位置とそのふたつ左の間を移動する障害物
そのため左の柱に開いている隙間は広くなったり狭くなったりを繰り返すだけで完全に閉じることはありませんが、中央の柱に開いている隙間は完全に閉じてしまう場合があるということになります。
それから半は#よりも16ピクセル下に設置されている障害物です。難易度を上げるために隙間を狭くしたいのですが、64ピクセルでは広すぎる、32ピクセルでは狭すぎて通れないので苦肉の策として。
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 |
public class FrisbeeGame { void CreateObstacles(List<Obstacle> starts, List<Obstacle> goals, ref int fieldWidth) { string fieldText = ""; if(StageNumber == 1) fieldText = Zero.Properties.Resources.frisbee_stage_1; if (StageNumber == 2) fieldText = Zero.Properties.Resources.frisbee_stage_2; if (StageNumber == 3) fieldText = Zero.Properties.Resources.frisbee_stage_3; if (StageNumber == 4) fieldText = Zero.Properties.Resources.frisbee_stage_4; if (StageNumber >= 5) fieldText = Zero.Properties.Resources.frisbee_stage_5; string[] vs = fieldText.Replace("\r", "").Split('\n'); int colMax = vs[0].Length; vs = vs.Where(s => s.Length == colMax).ToArray(); vs = vs.Reverse().ToArray(); int rowMax = vs.Length; for (int row = 0; row < rowMax; row++) { char[] chars = vs[row].ToArray(); for (int col = 0; col < colMax; col++) { if (chars[col] == '#') FixedObstacles.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); if (chars[col] == '半') FixedObstacles.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row - BLOCK_SIZE / 2, BLOCK_SIZE, BLOCK_SIZE)); if (chars[col] == 'S') starts.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); if (chars[col] == 'G') goals.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); if (chars[col] == '▽') { FixedObstacles.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); MovableObstacles.Add(new MovableObstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE, 0, -0.5, 64)); } if (chars[col] == '△') { FixedObstacles.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); MovableObstacles.Add(new MovableObstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE, 0, 0.5, 64)); } if (chars[col] == '▼') { FixedObstacles.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); MovableObstacles.Add(new MovableObstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE, 0, -0.5, 128)); } if (chars[col] == '▲') { FixedObstacles.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); MovableObstacles.Add(new MovableObstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE, 0, 0.5, 128)); } if (chars[col] == 'L') { FixedObstacles.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); MovableObstacles.Add(new MovableObstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE, -0.5, 0, 128)); } if (chars[col] == 'R') { FixedObstacles.Add(new Obstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE)); MovableObstacles.Add(new MovableObstacle(BLOCK_SIZE * col, BLOCK_SIZE * row, BLOCK_SIZE, BLOCK_SIZE, 0.5, 0, 128)); } } } fieldWidth = BLOCK_SIZE * colMax; } } |
更新時の処理に移動できる障害物を移動させる処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class FrisbeeGame { public void Update() { // 移動できる障害物を移動させる foreach (MovableObstacle obstacle in MovableObstacles) obstacle.Update(); Player.Update(); foreach (Spark spark in Sparks) spark.Update(); foreach (Spark star in Stars) star.Update(); } } |
Player.StageClearメソッドは以下のようになっています。ステージクリアが成立すると更新処理を止めるために死亡フラグをtrueにしています。そのため次のステージが生成されたら死亡フラグをクリアします。また時間の計測もリセットしています。
1 2 3 4 5 6 7 8 |
public class Player { public void StageClear() { IsDead = false; StartTime = DateTime.Now; } } |
Playerクラスの修正
障害物の種類が増えたため、当たり判定の処理が少し変更になります。
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 |
public class Player { bool IsMiss() { List<Position> positions = GetFrisbeeCorners(); foreach (Obstacle obstacle in _game.FixedObstacles) { if(positions.Any(p => obstacle.IsInside(p.X, p.Y))) return true; if (obstacle.IsInside((int)X, (int)Y)) return true; } foreach (Obstacle obstacle in _game.MovableObstacles) { if (positions.Any(p => obstacle.IsInside(p.X, p.Y))) return true; if (obstacle.IsInside((int)X, (int)Y)) return true; } return false; } } |
FrisbeeHubクラスの修正
クライアントサイドにマップのデータを送るとき、固定されている障害物と移動する障害物のデータをわけて送らなければなりません。
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 |
public class FrisbeeHub : Hub { static async Task SendMap(string connectionId) { if (!ClientProxyMap.ContainsKey(connectionId) || !Games.ContainsKey(connectionId)) return; FrisbeeGame game = Games[connectionId]; int blockSize = FrisbeeGame.BLOCK_SIZE; string xs1 = String.Join(",", game.FixedObstacles.Select(o => o.CenterX.ToString())); string ys1 = String.Join(",", game.FixedObstacles.Select(o => o.CenterY.ToString())); await ClientProxyMap[connectionId].SendAsync("SendFixedObstaclesToClient", xs1, ys1, blockSize, blockSize, game.MaxX); string xs2 = String.Join(",", game.MovableObstacles.Select(o => o.CenterX.ToString())); string ys2 = String.Join(",", game.MovableObstacles.Select(o => o.CenterY.ToString())); await ClientProxyMap[connectionId].SendAsync("SendMovableObstaclesToClient", xs2, ys2, blockSize, blockSize, game.MaxX); await ClientProxyMap[connectionId].SendAsync( "SendStartGoalToClient", game.Start.CenterX, game.Start.CenterY, game.Start.Width, game.Start.Height, game.Goal.CenterX, game.Goal.CenterY, game.Goal.Width, game.Goal.Height); string playerX = game.Player.X.ToString(); string playerY = game.Player.Y.ToString(); await ClientProxyMap[connectionId].SendAsync( "SendPlayerToClient", game.Player.X, game.Player.Y, FrisbeeGame.PLAYER_PADIUS, FrisbeeGame.PLAYER_THICKNESS); } } |
障害物が移動したらそれをクライアントサイドに送信しなければなりません。
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 |
public class FrisbeeHub : Hub { static async Task SendUpdateToClient(string id) { if (!ClientProxyMap.ContainsKey(id) || !Games.ContainsKey(id)) return; // プレイヤー、火花、星、移動する障害物の位置と経過時間をクライアントサイドに送信する await SendUpdatePlayerToClient(id); await SendUpdateSparksToClient(id); await SendUpdateStarsToClient(id); await SendUpdateObstaclesToClient(id); await SendUpdateTimeToClient(id); // クライアントサイドへのデータ送信終了 await ClientProxyMap[id].SendAsync("EndUpdateToClient"); } static async Task SendUpdatePlayerToClient(string id) { Player player = Games[id].Player; await ClientProxyMap[id].SendAsync( "UpdatePlayerToClient", player.X, player.Y, player.Angle, player.IsDead); } static async Task SendUpdateSparksToClient(string id) { List<Spark> sparks = Games[id].Sparks; string sparkXs = String.Join(",", sparks.Select(spark => spark.X).ToArray()); string sparkYs = String.Join(",", sparks.Select(spark => spark.Y).ToArray()); string sparkLifes = String.Join(",", sparks.Select(spark => spark.Life).ToArray()); await ClientProxyMap[id].SendAsync("UpdateSparksToClient", sparkXs, sparkYs, sparkLifes); } static async Task SendUpdateStarsToClient(string id) { List<Spark> stars = Games[id].Stars; string starXs = String.Join(",", stars.Select(star => star.X).ToArray()); string starYs = String.Join(",", stars.Select(star => star.Y).ToArray()); await ClientProxyMap[id].SendAsync("UpdateStarsToClient", starXs, starYs); } static async Task SendUpdateObstaclesToClient(string id) { List<MovableObstacle> obstacles = Games[id].MovableObstacles; string xs = String.Join(",", obstacles.Select(o => o.CenterX).ToArray()); string ys = String.Join(",", obstacles.Select(o => o.CenterY).ToArray()); await ClientProxyMap[id].SendAsync("UpdateObstaclesToClient", xs, ys); } static async Task SendUpdateTimeToClient(string id) { Player player = Games[id].Player; string time = ""; if (!player.IsGameOver) { TimeSpan ts = player.GetElapsedTime(); time = string.Format("{0:00}:{1:00}:{2:00}", ts.Minutes, ts.Seconds, ts.Milliseconds / 10); } await ClientProxyMap[id].SendAsync("EndUpdateTimeToClient", time); } } |
クライアントサイドにおける処理
クライアントサイドで追加する処理を示します。
最初に固定された障害物と移動できる障害物のふたつがサーバーサイドから送られてくるので、それぞれをシーンに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
let fixedObstacles = []; connection.on("SendFixedObstaclesToClient", function (xs, ys, width, height, max) { // 古い障害物をシーンから取り除く for (let i = 0; i < fixedObstacles.length; i++) scene.remove(fixedObstacles[i]); fixedObstacles = []; const material = new THREE.MeshLambertMaterial({ color: 0x004000 }); let xArray = xs.split(','); let yArray = ys.split(','); for (let i = 0; i < xArray.length; i++) { const geometry = new THREE.BoxGeometry(width, height, 128); let obj = new THREE.Mesh(geometry, material); obj.position.x = xArray[i]; obj.position.y = yArray[i]; obj.position.z = 0; scene.add(obj); fixedObstacles.push(obj); // シーンから取り除くことを考えて配列にも格納しておく } maxX = max; }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
let movableObstacles = []; connection.on("SendMovableObstaclesToClient", function (xs, ys, width, height) { // 古い障害物をシーンから取り除く for (let i = 0; i < movableObstacles.length; i++) scene.remove(movableObstacles[i]); movableObstacles = []; const material = new THREE.MeshLambertMaterial({ color: 0x004000 }); let xArray = xs.split(','); let yArray = ys.split(','); for (let i = 0; i < xArray.length; i++) { const geometry = new THREE.BoxGeometry(width, height, 128); let obj = new THREE.Mesh(geometry, material); obj.position.x = xArray[i]; obj.position.y = yArray[i]; obj.position.z = 0; scene.add(obj); movableObstacles.push(obj); // シーンから取り除くことを考えて配列にも格納しておく } }); |
更新処理が必要になったらサーバーサイドからUpdateObstaclesToClientが送信されます。この場合は該当するものを移動させます。
1 2 3 4 5 6 7 8 9 10 11 12 |
connection.on("UpdateObstaclesToClient", function (xs, ys) { if (xs == '') return; let xArray = xs.split(','); let yArray = ys.split(','); for (let i = 0; i < xArray.length; i++) { movableObstacles[i].position.x = xArray[i]; movableObstacles[i].position.y = yArray[i]; } }); |
それからEndUpdateToClientを受信したときの引数を変更したのとEndUpdateTimeToClientを追加したので以下も追加と変更が必要です。
connection.on(“EndUpdateTimeToClient”, function (time) {
textField1.innerHTML = time;
});
connection.on(“EndUpdateToClient”, function () {
if (maxX – CANVAS_WIDTH / 2 < cameraTargetX)
camera.position.x = maxX - CANVAS_WIDTH / 2;
else if (CANVAS_WIDTH / 2 < cameraTargetX)
camera.position.x = cameraTargetX;
else
camera.position.x = CANVAS_WIDTH / 2;
camera.position.y = 32 * 8
camera.position.z = 650;
camera.lookAt(new THREE.Vector3(camera.position.x, 32 * 8, 0));
renderer.render(scene, camera); // レンダリング
});