Unityで2D STGを作る(4日目)(終)
- こまごまとした修正
- スペカ名を右上に表示するように
- ボス撃破エフェクト追加
- ボス撃破後、少し待ってからクリア画面に行くように
- ボスマーカーを追加
- スペカ2実装。時止め処理追加
単にグローバルなところに時止めフラグを追加し、ボス以外のUpdateでそのフラグを見て何もしないだけ。
見栄えを考慮し、時止め中はボムが敵弾を消さないように(次にUpdateで処理をする際にDestroyするように) - WebGL用のビルド設定を追加
ここで問題が発生。
何故かProject Setting...→Player→Resolutionで指定したサイズになってくれない。
ChromeでもFireFoxでも同じ。
開発者ツールで見ると、gameControllerのdivのwidth、heightは指定したサイズ(1024*768)になっているが、Canvasのサイズが1163、872になってしまっている。
Standaloneでビルドしてexeで実行すると意図した画面サイズになる。 これはググっても同様の症状が見当たらず解決していない。
暫定対応として、以下を行った - index.htmlにCanvasのサイズ調整を行うボタンを追加
参考:UnityのWebGLビルドのHTMLテンプレート|npaka|note WebGLTemplatesフォルダを作成し、ビルド時に任意のhtmlを使えるように。
ここにボタンを配置し、クリック時に以下が実行されるように
function resize(canvas) { var canvas = document.getElementById('#canvas'); canvas.width = 1024; canvas.height = 768; canvas.clientWidth = 1024; canvas.clientHeight = 768; }
- タイトルからゲーム開始時にScreen.widthと想定する幅を比較して違ったらダイアログを表示し、リサイズボタンを押させる
上記の対応でゲーム部分は動作するが、ボタンの当たり判定がずれるという問題がある。
これは致命的ではないので無視した。
ただし、キーボードで常に選択できるようにはした
if (UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject == null) { UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(titleButton.gameObject); }
- GitHub Pagesに公開
参考:am1tanaka.hatenablog.com
GitHubに「自分のユーザーアカウント.github.io」でリポジトリを作成してそこにコミットすればいい。
https://{ユーザアカウント}.github.io でアクセスできる
出来上がったのが以下
https://daybreaksnow.github.io
クオリティを上げるべきところはたくさんあるが、今回はマリン船長の誕生日お祝い企画ということで、いったんここで終わり。
Unityで2D STGを作る(3日目)
- ボスの被ダメージ処理を追加
- IPlayerShotインタフェースを作成し、自機弾、ボムに実装させる
- ボスは被弾時にGameObjectからIPlayerShotを取得してダメージを取得
- ボスの残ライフ表示バー作成
// 残ライフに応じてバーの長さを調整 private void updateLifeBarLength() { float maxLife = modeLifeMap[restModeNum]; float per = life / maxLife; Vector3 newScale = new Vector3(per, lifeBarObj.transform.localScale.y, lifeBarObj.transform.localScale.z); lifeBarObj.transform.localScale = newScale; Debug.Log("newScale:" + newScale); } // 画像はpositionを中心にして描画されるので、画像の大きさに応じて位置をずらす private void updateLifeBarPos() { float leftX = GameManager.getLeftByScreen() + 50; float barWidth = lifeBarObj.GetComponent<SpriteRenderer>().bounds.size.x; Vector3 worldPos = GameManager.toWorldPoint(leftX + barWidth / 2, 0); lifeBarObj.transform.position = new Vector3(worldPos.x, lifeBarObj.transform.position.y, lifeBarObj.transform.position.z); Debug.Log("newPos:" + lifeBarObj.transform.position); }
- ボスの残形態数を表すバーを作成
public void addRestModeBar(int index) { // 画像サイズを使いたいので一旦インスタンス化 GameObject icon = gameManager.instantiate("LifeBar", lifeBarObj.transform.position); // 残り形態のバーの配置領域 float baseLeft = GameManager.getLeftByScreen() + 10; float baseRight = GameManager.getLeftByScreen() + 40; float areaWidth = baseRight - baseLeft; // 配置領域に収まるよう長さを調整 // バーの長さ×残り形態 + スペース×(残り形態-1)が上記に収まるように // barWidth * restModeNum + space * (restModeNum - 1) = areaWidth; // barWidth = (areaWidth - space * (restModeNum - 1)) / restModeNum; float space = 2; float barWidth = (areaWidth - space * (restModeNum - 1)) / restModeNum; // scaleを調整してバーの長さを調整 float originalWidth = icon.GetComponent<SpriteRenderer>().bounds.size.x; float scale = barWidth / originalWidth; icon.transform.localScale = new Vector3(scale, icon.transform.localScale.y, icon.transform.localScale.z); // indexに応じて配置 float startX = baseLeft + barWidth / 2; float spacing = barWidth + space; float x = startX + (index - 1) * spacing; icon.transform.position = new Vector3(GameManager.toWorldPoint(x, 0).x, icon.transform.position.y, icon.transform.position.z); restModeBarIcons.Push(icon); }
- ボスのモード切替を追加
void OnTriggerEnter2D(Collider2D collider) { // 自機、自機弾以外とはそもそも接触判定をしないので考慮不要 // 自機と当たった場合は自機側で処理するので何もしない if (collider.CompareTag("Player")) { return; } // 無敵中は終わり if (isInvincible()) { return; } // ボムか自機弾と当たったらダメージを受ける IPlayerShot shot = collider.GetComponent<IPlayerShot>(); life -= shot.GetDamage(); // ライフバーの長さ調整 updateLifeBarLength(); updateLifeBarPos(); if (life <= 0) { if (restModeNum == 0) { // TODO 時間をおいてから life = 0; clearAllEnemyShot(); // ゲームクリア画面へ遷移 SceneManager.LoadScene("EndScene"); } else { // 次のゲージへ restModeNum--; life = modeLifeMap[restModeNum]; updateLifeBarLength(); updateLifeBarPos(); loseRestModeBar(); BossState nextState; switch (restModeNum) { case 1: nextState = new Spell1State(this); break; case 0: nextState = new Spell2State(this); break; default: throw new Exception("illegal restModeNum:" + restModeNum); } state = new ConnectState(this, nextState); // 敵弾はクリア clearAllEnemyShot(); } } Debug.Log("hit Boss"); } private class ConnectState : BossState { private Boss1 boss; private BossState nextState; public ConnectState(Boss1 boss, BossState nextState) { this.boss = boss; this.nextState = nextState; } public void Update() { // モード切替時とその後の100Fは無敵 boss.invincibleCount = 100; float moveX = 0; float moveY = 0; float speed = 4; //初期位置に移動 // TODO スムーズに移動するように Vector2 baseWorldPos = new Vector2(boss.gameManager.getCenter().x, boss.getBaseY()); if (Math.Abs(baseWorldPos.x - boss.transform.position.x) > speed * 2) { if (baseWorldPos.x < boss.transform.position.x) { moveX = -speed; } else { moveX = speed; } } if (Math.Abs(baseWorldPos.y - boss.transform.position.y) > speed * 2) { if (baseWorldPos.y < boss.transform.position.y) { moveY = -speed; } else { moveY = speed; } } boss.transform.Translate(moveX, moveY, 0); // 移動が終わったら次のステートへ if (moveX == 0 && moveY == 0) { boss.state = nextState; } } }
- キー操作で移動するように、ショットは押しっぱなしで発射するように、低速移動を追加
- 開発中は特定キー押下で無敵になるように
Application.isEditorで判断できる - Resouces.loadの結果をキャッシュするように
必要ないかもしれないが、開発時にResource ID out of range in GetResourceのエラーが出たので
public GameObject instantiate(String prehabName, Vector3 worldPos) { String name = "Prefabs/" + prehabName; GameObject prehab; if (prehabCache.ContainsKey(name)) { prehab = prehabCache[name]; } else { prehab = (GameObject)Resources.Load(name); prehabCache.Add(name,prehab); } return (GameObject)Instantiate(prehab, worldPos, Quaternion.identity); }
- 敵弾の画像サイズに応じて当たり判定も調整されるように
public void setSprite(string spriteName) { SpriteRenderer renderer = GetComponent<SpriteRenderer>(); renderer.sprite = gameManager.loadSprite(spriteName); // XXX 画像は縦横同じサイズという想定 float spriteSize = renderer.bounds.size.x; // 画像サイズに応じて当たり判定を調整 if (spriteSize != SIZE) { float scale = spriteSize / (float)EnemyShot.SIZE; GetComponent<CircleCollider2D>().radius = GetComponent<CircleCollider2D>().radius * scale; } }
- ボスの通常弾幕を作成
private class Normal1State : BossState { private Boss1 boss; private GameManager gameManager; private float startAngle = 270; //真下 private Vector2 moveVector; public Normal1State(Boss1 boss) { this.boss = boss; this.gameManager = boss.gameManager; } private int count; public void Update() { // 0~100は何もしない if (count < 100) { } // nWay弾を角度を変えて定期的に撃つだけ else if (count < 200) { if (count % 15 == 0) { int n = 16; for (int i = 0; i < n; i++) { float angle = startAngle + (360.0f / n) * i; float speed = 10; Vector2 moveVector = GameManager.getMoveVector(speed, angle); GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position); enemyShot.GetComponent<EnemyShot>().init(gameManager, new StraightShotStrategy(boss.gameManager, enemyShot, moveVector)); } startAngle += 5; } } // 200~300は何もしない if (count < 300) { } // 300になったらランダムな方向に移動開始 else if (count == 300) { float speed = 0.5f; float angle = UnityEngine.Random.Range(0, 360); moveVector = GameManager.getMoveVector(speed, angle); // ゲーム領域外に近寄らなすぎないように調整 moveVector = boss.fixMoveVector(moveVector); } else if (count <= 400) { // 300~400は移動しながらショット boss.transform.Translate(moveVector); if (count % 15 == 0) { int n = 16; for (int i = 0; i < n; i++) { float angle = startAngle + (360.0f / n) * i; float speed = 10; Vector2 moveVector = GameManager.getMoveVector(speed, angle); GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position); enemyShot.GetComponent<EnemyShot>().init(gameManager, new StraightShotStrategy(boss.gameManager, enemyShot, moveVector)); } startAngle -= 5; } } count++; // 400になったら最初に戻る if (count == 400) { count = 0; } } }
- ボスのスペカ1を作成
体力半分以上:nWay弾が画面端近くに行ったら戻ってくる(実際は指定フレーム数で戻るようにしてるだけで細かな調整によるもの)
体力1/4以上:回転するnWay弾が画面端近くに行ったら戻ってくる(時計周りと反時計回りの繰り返し)
体力1/4未満:時計回りと反時計周りのnWay戻り弾を同時に発射+ゆっくりとした自機狙い弾
private class Spell1State : BossState { private Boss1 boss; private GameManager gameManager; private float maxLife; private float prevLife; private Spell1SubState subState; public Spell1State(Boss1 boss) { this.boss = boss; this.gameManager = boss.gameManager; maxLife = boss.life; prevLife = boss.life; subState = new FirstState(boss); } private interface Spell1SubState { void Update(); } //体力半分以上 private class FirstState : Spell1SubState { private int count; private float startAngle = 270; //真下 private Vector2 moveVector; private Boss1 boss; private GameManager gameManager; public FirstState(Boss1 boss) { this.boss = boss; this.gameManager = boss.gameManager; } public void Update() { // 0~100は何もしない if (count < 100) { } // nWay弾を角度を変えて定期的に撃つだけ else if (count < 200) { if (count % 20 == 0) { int n = 16; const int stopStartCount = 75; const int stopCount = 45; const int slowCount = stopStartCount * 3 / 10; const float returnSpeedRate = 0.8f; for (int i = 0; i < n; i++) { float angle = startAngle + (360.0f / n) * i; float speed = 9f; Vector2 moveVector = GameManager.getMoveVector(speed, angle); GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position); enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnStraightShotStrategy(boss.gameManager, enemyShot, moveVector, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.GRAY_SPHERE_48)); enemyShot.GetComponent<EnemyShot>().setSprite(ImageManager.BLACK_SPHERE_48); } startAngle += 6; } } // 200~300は何もしない if (count < 300) { } // 300になったらランダムな方向に移動開始 else if (count == 300) { float speed = 0.5f; float angle = UnityEngine.Random.Range(0, 360); moveVector = GameManager.getMoveVector(speed, angle); // ゲーム領域外に近寄らなすぎないように調整 moveVector = boss.fixMoveVector(moveVector); } else if (count <= 400) { // 300~400は移動しながらショット boss.transform.Translate(moveVector); if (count % 20 == 0) { int n = 16; const int stopStartCount = 75; const int stopCount = 45; const int slowCount = stopStartCount * 3 / 10; const float returnSpeedRate = 0.8f; for (int i = 0; i < n; i++) { float angle = startAngle + (360.0f / n) * i; float speed = 9f; Vector2 moveVector = GameManager.getMoveVector(speed, angle); GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position); enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnStraightShotStrategy(boss.gameManager, enemyShot, moveVector, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.GRAY_SPHERE_48)); enemyShot.GetComponent<EnemyShot>().setSprite(ImageManager.BLACK_SPHERE_48); } startAngle -= 6; } } count++; // 400になったら最初に戻る if (count == 400) { count = 0; } } } //体力半分以下、1/4以上 private class SecondState : Spell1SubState { private int count; private float startAngle = 270; //真下 private Vector2 moveVector; private Boss1 boss; private GameManager gameManager; // private int straightAngleSign = 1; public SecondState(Boss1 boss) { this.boss = boss; this.gameManager = boss.gameManager; } public void Update() { // 0~100は何もしない if (count < 100) { } else if (count < 250) { if (count % 35 == 0) { int n = 16; const float speed = 7.5f; //反時計回り const float angleSpeed = -1; const int stopStartCount = 90; const int stopCount = 45; const int slowCount = 25; const float returnSpeedRate = 0.8f; for (int i = 0; i < n; i++) { float initialAngle = startAngle + (360.0f / n) * i; GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position); enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnRollingShotStrategy(boss.gameManager, enemyShot, speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.GRAY_SPHERE_48)); enemyShot.GetComponent<EnemyShot>().setSprite(ImageManager.BLACK_SPHERE_48); } startAngle += 6; } } // 250~350は何もしない if (count < 350) { } // 350になったらランダムな方向に移動開始 else if (count == 350) { float speed = 0.5f; float angle = UnityEngine.Random.Range(0, 360); moveVector = GameManager.getMoveVector(speed, angle); // ゲーム領域外に近寄らなすぎないように調整 moveVector = boss.fixMoveVector(moveVector); } else if (count <= 500) { // 350~500は移動しながらショット boss.transform.Translate(moveVector); if (count % 30 == 0) { int n = 16; const float speed = 7.5f; //時計回り const float angleSpeed = 1; const int stopStartCount = 90; const int stopCount = 45; const int slowCount = 25; const float returnSpeedRate = 0.8f; for (int i = 0; i < n; i++) { float initialAngle = startAngle + (360.0f / n) * i; GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position); enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnRollingShotStrategy(boss.gameManager, enemyShot, speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.GRAY_SPHERE_48)); enemyShot.GetComponent<EnemyShot>().setSprite(ImageManager.BLACK_SPHERE_48); } startAngle += 6; } } count++; // 500になったら最初に戻る if (count == 500) { count = 0; straightAngleSign = -straightAngleSign; } } } private class ThirdState : Spell1SubState { private int count; private float startAngle = 270; //真下 private Vector2 moveVector; private Boss1 boss; private GameManager gameManager; public ThirdState(Boss1 boss) { this.boss = boss; this.gameManager = boss.gameManager; } public void Update() { // 0~100は何もしない if (count < 100) { } // 100になったらランダムな方向に移動開始 else if (count == 100) { float speed = 0.5f; float angle = UnityEngine.Random.Range(0, 360); moveVector = GameManager.getMoveVector(speed, angle); // ゲーム領域外に近寄らなすぎないように調整 moveVector = boss.fixMoveVector(moveVector); } else if (count < 300) { // 100~300は移動しながらショット boss.transform.Translate(moveVector); if (count % 30 == 0) { int n = 12; const float speed = 5.5f; const float angleSpeed = 0.5f; const int stopStartCount = 135; const int stopCount = 45; const int slowCount = 30; const float returnSpeedRate = 0.8f; for (int i = 0; i < n; i++) { float initialAngle = startAngle + (360.0f / n) * i; // 見栄えを考慮して描画順序を調整 if (i % 2 == 0) { createRetrunRollingShot(speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.RED_SPHERE_48, ImageManager.GRAY_SPHERE_48); createRetrunRollingShot(speed, initialAngle, -angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.BLUE_SPHERE_48, ImageManager.GRAY_SPHERE_48); } else { createRetrunRollingShot(speed, initialAngle, -angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.BLUE_SPHERE_48, ImageManager.GRAY_SPHERE_48); createRetrunRollingShot(speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.RED_SPHERE_48, ImageManager.GRAY_SPHERE_48); } } startAngle -= 6; } } count++; // 定期的に自機狙い弾を放つ if (count % 200 == 150) { float speed = 0.75f; float angle = GameManager.getPlayerAngle(boss.transform.position); Vector2 moveVector = GameManager.getMoveVector(speed, angle); GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position); enemyShot.GetComponent<EnemyShot>().init(gameManager, new StraightShotStrategy(boss.gameManager, enemyShot, moveVector)); } // 500になったら最初に戻る if (count == 500) { count = 0; } } private GameObject createRetrunRollingShot(float speed, float initialAngle, float angleSpeed, int stopStartCount, int stopCount, int slowCount, float returnSpeedRate, String spriteName, String returnSpriteName) { GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position); enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnRollingShotStrategy(boss.gameManager, enemyShot, speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, returnSpriteName)); enemyShot.GetComponent<EnemyShot>().setSprite(spriteName); return enemyShot; } } public void Update() { float firstLife = maxLife / 2; float secondLife = maxLife / 4; if (prevLife >= firstLife && boss.life < firstLife) { subState = new SecondState(boss); } else if (prevLife >= secondLife && boss.life < secondLife) { subState = new ThirdState(boss); } subState.Update(); prevLife = boss.life; } }
スペカ1のラストのキャプチャ
Unityで2D STGを作る(2日目)
- ボスと敵弾を作成
- 当たり判定を追加。敵弾同士などで衝突判定をしないようレイヤ追加
参考:Unity - Unityで特定のオブジェクトと衝突したときだけisTriggerをtrueにしたい|teratail
各オブジェクト毎にレイヤを設定し、「Edit」→「Project Settings」→「Physics 2D」の「Layer Collision Matrix」で衝突可能な組み合わせを設定できる
Player,PlayerShot,Bomb,Enemy,EnemyShotを追加 - Collidarのscaleがピクセルになるよう、カメラのscale、テクスチャのPPUを修正
scaleを画面の縦幅/2の値に、PPUを1にした - 被弾時のSEを追加
その3 スクリプト内だけでSEを鳴らすを参考に、スクリプトのみで再生できるように
soundPlayer = new GameObject("SoundPlayer"); audioSource = soundPlayer.AddComponent<AudioSource>(); AudioClip clip = (AudioClip)Resources.Load("Audio/" + audioName); audioSource.PlayOneShot(clip);
- 被弾時に徐々に消滅→画面下から出現、となるように
PlayerにStateパターンを適用。 通常時→消滅中(くらいボム待ち)→復活中、の3ステートに
・PlayerのUpdate
void Update() { state.Update(); // 無敵時間制御 if (invinsibleTime > 0) { invinsibleTime--; if (invinsibleTime == 0) { //透過状態をもとに戻す setAlpha(1.0f); } } //ボム時間制御 if (bombModeTime > 0) { bombModeTime--; } }
・消滅待ちのState
public void Update() { // ボムのみ使用可能 if (player.isUseBomb()) { player.bomb(); // サイズをもとに戻す player.transform.localScale = new Vector3(1, 1, 1); // 通常モードに戻す player.state = new NormalState(player); Debug.Log("食らいボム成功"); return; } // 徐々にサイズを小さくする。猶予は20Fくらいとして、0.05ずつ減らす Vector3 newScale = new Vector3(player.transform.localScale.x - 0.05f, player.transform.localScale.y - 0.05f, player.transform.localScale.z); // 消滅したら復活モードに移行 if (newScale.x <= 0) { // サイズをもとに戻す player.transform.localScale = new Vector3(1, 1, 1); // 画面外の中央下に配置 float x = (GameManager.getLeftByScreen() + GameManager.getRightByScreen()) / 2; float y = GameManager.getBottomByScreen() - GameManager.GAME_AREA_HEIGHT / 8; Vector3 newPos = GameManager.toWorldPoint(x, y); player.transform.position = newPos; if (player.life > 0) { // 残機を減らす player.life--; gameManager.loseLifeIcon(); // ボム数を戻す gameManager.clearBombIcon(); player.initBombCount(); // 復活モードに移行 player.state = new RecoverState(player); } else { //タイトルに戻る SceneManager.LoadScene("TitleScene"); } return; } player.transform.localScale = newScale; Debug.Log(player.transform.localScale); }
・復帰中のState
public void Update() { // 少しずつ画面上に上がる Vector3 move = new Vector3(0, speed, 0); player.transform.Translate(move); // 規定位置まで上がったら通常モードに戻す if (player.transform.position.y > baseY) { player.state = new NormalState(player); } }
- 残機、ボムの残数の表示を追加
- ボムの発射処理と敵弾の消滅処理を追加
- メインシーンのBGM追加
AudioSource mainBGMSource = soundPlayer.AddComponent<AudioSource>(); mainBGMSource.loop = true; mainBGMSource.volume = mainBGMSource.volume / 5; //BGMなので小さめに AudioClip clip = (AudioClip)Resources.Load("Audio/" + "Main"); mainBGMSource.clip = clip; mainBGMSource.Play();
- タイトル画面、クレジット画面を作成。シーン遷移を作成
基本は0から2Dアクションバトルゲームを作ろう!⑧ビルドをしてゲームを遊んでみようを参考に。
ボタンをキーボード選択で選択できるように
参考:【Unity】キーボードからカーソル選択するセレクトリストを作る(1) - ペンギンとザリガニ- ButtonのNavigationをVerticalに
- EventSystemのFirst Selectedに初期選択されるボタンを設定
昨日とあまり変わりはないが画面キャプチャ
Unityで2D STGを作る(1日目)
マリン船長(Marine Ch. 宝鐘マリン - YouTube)にはまって何かコンテンツを作ろうと思い、学生時代に作ったことのあるSTGならサクッと作れるだろう、ということで開始。船長は東方好きなのでSTGはシナジーもあるし。
利用するUnityのバージョンは2019.4.4。
事前知識としては以下のブログを読んだ程度。
Unityで0から2Dアクションバトルゲームを作ろう!①ゲーム画面の作成
今日やったことは以下
GitHubと連携する
参考:【超初心者向け】Unityのプロジェクトを、GitHub for Unityを使って超簡単にバックアップする方法 - Qiita
コマンドラインからgitで直接やってもいいが、GitHub for Unityという専用のパッケージがあるようなのでそれを使う(.gitignore作るのが面倒だし)
実施内容は以下
再配布禁止の素材を使うこともあると思うので、private repositoryで作成した
- Visual Studioで保存時に自動で整形されるように
参考:Visual Studio で保存時にコードを自動整形する|へっぽこプログラマーの備忘録
拡張機能→拡張機能の管理→Format docで検索して「Format document on save」をダウンロード - 画面サイズを指定
東方が640×480だったと思うのでそれに倣おうと思ったが、作業しているPCが1920×1080なのでかなり小さい。
1024×768にした。 - プレイヤー画像の作成
船長の三面図をドット絵ナニカで変換して、その画像の色をスポイトで取ってそれっぽく作成 - プレイヤーの作成
0から2Dアクションバトルゲームを作ろう!②プレイヤーキャラを動かそう の通り、マウス位置に追従するだけ - GameManagerを作成、プレイヤーをプレハブに
ほとんどのオブジェクトは動的に作られると思うので、プレイヤーだけ最初からオブジェクトができてるのも変では?と思い、GameManagerでインスタンス化するように。
プレハブはResources.Loadで読み込み。 - 背景画像を探す
「海賊 背景」とか「海図 背景」で検索して以下が見つかったのでいったんこれ。1024×768になるよう縮小&端をカット
[フリーイラスト] 羅針図と古地図 - パブリックドメインQ:著作権フリー画像素材集 - 画像が等倍で表示されるようカメラのスケールを調整
だいぶはまった。
Choosing the resolution of your 2D art assets2D アートアセットの解像度選択 - Unity Technologies BlogやUnity 2D ゲームの画面(メインカメラ)サイズの選択と設定例 - sh1’s diaryを見たところ、CameraのScaleを画面サイズの縦幅の1/200の値(1024×768であれば、768の1/200で3.84)にしないと等倍で表示されないようだ - ゲーム領域の背景画像を探す
一旦以下を使うことに。ゲーム領域は640*720にした
「空と海の蒼いグラデーション」の画像素材を無料ダウンロード(1)フリー素材 BEIZ images - プレイヤーをゲーム領域外に出ないようにする
private void fixPosition() { // 左端 float width = GetComponent<SpriteRenderer>().bounds.size.x; float left = transform.position.x - width / 2; if (left < gameManager.GAME_AREA_LEFT) { transform.position = new Vector3(gameManager.GAME_AREA_LEFT + width / 2, transform.position.y, transform.position.z); } //右端 float right = transform.position.x + width / 2; if (right > gameManager.GAME_AREA_RIGHT) { transform.position = new Vector3(gameManager.GAME_AREA_RIGHT - width / 2, transform.position.y, transform.position.z); } // 上端 float height = GetComponent<SpriteRenderer>().bounds.size.y; float top = transform.position.y + height / 2; if (top > gameManager.GAME_AREA_TOP) { transform.position = new Vector3(transform.position.x, gameManager.GAME_AREA_TOP - height / 2, transform.position.z); } // 下端 float bottom = transform.position.y - height / 2; if (bottom < gameManager.GAME_AREA_BOTTOM) { transform.position = new Vector3(transform.position.x, gameManager.GAME_AREA_BOTTOM + height / 2, transform.position.z); } }
- 自機の弾画像作成、自機の弾発射処理作成
updateの処理は単純にこれだけ
//画面上にしか行かない前提 Vector2 moveVector = new Vector2(0, speed); transform.Translate(moveVector); // 完全に画面外に出たら破棄 if (!GetComponent<Renderer>().isVisible) { Destroy(gameObject); }
- 背景画像のゲーム領域を透過するように
弾がゲーム領域外に描画されないよう、ゲーム領域背景→プレイヤー等→全体背景と描画されるように。そのために全体背景のゲーム領域部分を塗りつぶして透過させた
ここまでの画面キャプチャはこんな感じ
[Redmine]Redmineのマイグレーションを行う
CentOS6からCentOS7にRedmineをマイグレーションし、利用するDBMSもMySQLからPostgreSQLに変更した際の作業。
マイグレーション先
- CentOS7
- Redmine3.4.2
- PostgreSQL9.2
手順
マイグレーションに失敗してもよいよう、検証用マシンを用意して以下の手順で行った。
Redmineのアップグレード
1. 検証機にマイグレーション元と同じRedmineの環境を用意、DBのダンプ、添付ファイルを復元
過去の記事の通り
[Linux]RedmineのDBのバックアップを取る - daybreaksnow's diary
2. 検証機のRedmineをマイグレーション先のRedmineのバージョンにアップグレード
こちらを参考
アップグレード — Redmine Guide 日本語訳
2-1. Redmineのダウンロード、解凍、配置
wget http://www.redmine.org/releases/redmine-3.2.4.tar.gz tar xvzf redmine-3.2.4.tar.gz mv redmine-3.2.4 /var/lib/redmine-3.2.4
2-2. DB設定のコピー
cp -a /var/lib/redmine/config/configuration.yml /var/lib/redmine-3.2.4/config/configuration.yml cp -a /var/lib/redmine/config/database.yml /var/lib/redmine-3.2.4/config/database.yml
2-3. 添付ファイルコピー
cp -ar /var/lib/redmine/files/ /var/lib/redmine-3.2.4/
2-4. プラグインコピー
cp -ar /var/lib/redmine/plugins/ /var/lib/redmine-3.2.4/
2-5. 依存するツールをインストール
bundle install --without development test bundle exec rake generate_secret_token
2-6. DBのマイグレーション
bundle exec rake db:migrate RAILS_ENV=production
bundle exec rake redmine:plugins:migrate RAILS_ENV=production
2-8. キャッシュクリア
bundle exec rake tmp:cache:clear tmp:sessions:clear
2-9.「RackBaseURI /redmine-3.2.4」の行を追加
vi /etc/httpd/conf.d/passenger.conf
2-10. httpdに公開
ln -s /var/lib/redmine-3.2.4/public redmine-3.2.4
2-7.のプラグイン用DBのマイグレーションでタグ付けプラグインがこけたので、そのプラグインは削除して実行した(対して使っていなかったので)
データベースの移行
MySQLのダンプをPostgreSQL用に変換して取得することを考えていたが、RailsのDBはyaml_dbを使うとプレインテキストでバックアップが取れるということなのでyaml_dbを使った。
参考:
yaml_dbを使ってMySQLからPostgreSQLにRedmineを移行した - 烏龍茶より麦茶派です
1. yaml_dbのインストール
1-1. /var/lib/redmine/Gemfileに以下の行を追加
gem "yaml_db"
1-2. /var/lib/redmineで以下のコマンドを実行
bundle update bundle install --without development test
RAILS_ENV=production bundle exec rake db:data:dump
これでredmine/db/data.ymlができる。これをマイグレーション先の同ディレクトリに持っていく
3. マイグレーション先で復元
RAILS_ENV=production bundle exec rake db:data:load
ここで2-7.で削除したプラグインが使っていたテーブルが復元できないと言われたので、手動でdata.ymlからそのテーブルの情報を削除した。
[Subversive]削除された履歴を復元する
削除→新規追加などしてしまい、過去の履歴が辿れなくなった場合の対応。
逆マージを使う。
状況
リビジョン20でファイル追加
リビジョン21でファイル更新
リビジョン22でファイル削除&同名ファイルを新規追加
1.逆マージ
チーム→マージを選択して、改訂で取り消したいリビジョン(22)を選択
Reverse mergeにチェックし、OK
成功すると、対象ファイルのマークが変わる
2.修正を反映
マージされたファイルにrev22の修正を反映
[SVN]さくらのVPSにSVNリポジトリを立てる
参考:
Subversion環境をCentOSで構築 - 無理なものは無理
10分で作る、Subversionレポジトリ (CentOS 版) - ishikawa84g's blog
mod_dav_svnのインストール
必要なパッケージがインストールされているか確認
yum list installed | grep subversion yum list installed | grep mod_dav_svn
subversionはインストール済みだったが、mod_dav_svnはなかった。
インストールする。
yum install mod_dav_svn
SVNリポジトリの作成
テスト用にsandboxというリポジトリを作る。trunkとかbranchはひとまず無視。
mkdir /var/www/svn svnadmin create /var/www/svn/sandbox chown -R apache:apache /var/www/svn
Apache の設定(Basic 認証)
BASIC認証用のユーザ、パスワード設定
htpasswdコマンドを使って、ユーザ、パスワードを設定する
htpasswd -c /etc/httpd/.htpasswd ユーザ名
実行後、パスワードが聞かれるので入力。
パスワードはデフォルトではcrypt関数で暗号化されて保存される。(Cのcrypt関数のこと?)
- cは新規ファイル作成という意味なので、複数ユーザを追加する際は、-cは除く。
-cが付いていると、既存のファイルがあっても上書きされてしまうので注意。
apacheとSVNの連携
mod_dav_svnのインストールにより、以下のファイルが作成されている。これを編集する。
/etc/httpd/conf.d/subversion.conf
・Locationタグをコメントアウト
・Locationタグのパスを、アクセスしたいURLにする(reposをsvnに変更した)
・AuthUserFileのパスをさっき作ったパスに変更
<Location /svn> DAV svn SVNParentPath /var/www/svn # # Limit write permission to list of valid users. # <LimitExcept GET PROPFIND OPTIONS REPORT> # # Require SSL connection for password protection. # # SSLRequireSSL AuthType Basic AuthName "Authorization Realm" AuthUserFile /etc/httpd/.htpasswd Require valid-user # </LimitExcept> </Location>
httpdを再起動すれば終わり。
今回の例だと、http://(IPアドレス)/svn/sandboxにアクセスすればリポジトリの中身が見える。