UnityでのCosmosの開発の物語

私の記事では、ジャンルの選択と設定、最終クレジットでの終了、開発プロセス中に踏み込んだすべてのレーキなど、2週間でゲームを作成した経験を説明しようとしています。 ゲーム開発の経験が豊富な人にとっては、この記事はたいてい面白くないでしょうが、開発者を始めた人は自分にとって興味深いものを見つけることができると思います。





名前はゲームのその部分であり、最後の瞬間まで延期された作業です。 その結果、残念ながら失敗する価値のあるものは何もありませんでした。



ジャンル、設定、プロット



コンテスト、テーマ-「選択」(好きなように解釈し、呼ばれる)、用語-2週間という、かなり標準的に始まりました。 私は2Dゲームのわずかな拒絶に苦しんでいるので、2D / 3Dの問題はまったく立ちませんでした。 このジャンルと設定ではすでに難しくなっていたため、「スペースレンジャー」と「メカノイド」という100時間以上を費やした2つのお気に入りのゲームから始めることにしました。 そのため、私はこのジャンルを決定しました-エリートの仕組みに基づいて構築されたゲームは、もちろん大幅に削減されました。 このジャンルの論理的な設定はSci-fiでした。シーンはスペースです。2週間では、最低限の価値のある「グラウンドレベル」を作成することは非常に困難だからです。



必要なコンテンツの量を最小限にするために、生成された惑星システムを放棄し、全体としてあらゆる規模から放棄することが決定されました。 ゲーム全体が1つの小惑星帯内で行われ、いくつかのセクターに分割されています。興味のあるポイントの数は最小限です。 しかし、そのような決定はゲームの他の側面に時間を残しました。世界の研究を掘り下げれば、他のすべてに時間は残りません。





初日、インターフェースのスケッチと一連の機能が作成され、次の14日間で実装されました。



プロットで、私はさらに野barに行動しました(後で判明しましたが、無駄になりました)-GGのベースが取られた小惑星帯に侵入する神秘的な侵略者について思いついた最初のアイデアが基礎として採用されました。 詳細については、ストーリーの割り当てを考え出すのと同じように、あとで検討する必要がありました。



先が最も重要でした-機能。



特徴



私は本当に最善を尽くして、できるだけ多くの異なるチップを実装したかったのですが、それらの多くには1つの目標しかありませんでした。 これらの部品の1つはホーミングミサイルでした。 1回の攻撃で小さな敵を爆発させる能力があるにもかかわらず、ゲームをプレイした人が実際にそれらを実際に使用したことはなかったようです。 同時に、それらを作成するにはかなりの時間がかかりました。



一般に、主な機能のおおよそのリストは次のようになりました。





リストはかなり大きいことが判明しましたが、何らかの方法で、これらのすべての機能がさまざまな手の込んだ度合いでゲームで実現されました。



開始:最初のモデルと空間内の動き



ジャンルと機能セットで決定された後、3Dエディターで2つのモデル(主人公の宇宙船と敵の1つ)がすばやくスケッチされました。





敵船の原始性と立体性には陰謀の正当性があります。



そして、ついに、プログラミングを開始することが可能になりました。



最初のステップは、宇宙船とカメラの動きを実装することでした。



動きは非常に簡単に実装され、慣性やその他の現実的なものはありません。 プレイヤーの船はリジッドボディを持つモデルで、各ティックには角速度と並進速度が与えられます。 角速度を取得するには、中心からの変位ベクトルをマウスから取得し、各ティックでマウスの動きの現在のデルタを変位に追加し、ベクトル自体に1未満の係数を掛けます。船の小さな慣性の錯覚で、かなり滑らかな制御が得られます。



並進運動の場合はさらに簡単です。船はローカルの「前方」の方向に移動し、W / Sキーを押すと速度が徐々に増加または減少します。



カメラを使用すると、すべてが非常に簡単になりました。 額の船にカメラを取り付けるのは悪い考えです。したがって、カメラと船は接続されていませんが、カメラには、船の後ろから狙っている目標点と回転角度があります。 現在のポイントとターゲットポイントが補間されるため、「スムーズトラッキングカメラ」の効果が得られます。



速度の感覚を与えるために、船の加速で、カメラの視野はわずかに増加しますが、減速しながら、それに応じて減速します。





あなたは船が不快にひきつる様子を見ることができます。 これは、船の剛体設定で補間を有効にすることで解決しました。



隙間を埋める



空の黒いスペースでの移動は、どういうわけか面白くない。 したがって、次のステップは、宇宙をオブジェクトで満たすことでした。

この設定では小惑星帯がゲームの会場であったため、タスクは非常に簡単に解決されました。数百立方キロメートルのスペースに、数百の小惑星がランダムに散らばっていました。 ランダム生成のシードは事前に保存されていたため、ベースまたはクエストアイテムが小惑星内にあることを心配する必要はありませんでした。





遠くからはこんな感じです。



大きな「活気」とダイナミズムのために、大きな小惑星に加えて、小さな「スターダスト」が追加されました。 これらのささいなことは、場所全体ではもはや生成されませんでしたが、移動する船の周りに直接作成されました。



小惑星トリビアジェネレーターコード
public class MiniAsteroids : MonoBehaviour { private Transform tx; private GameObject[] asteroids = new GameObject[155]; public GameObject[] prefabs = new GameObject[2]; public int asteroidsMax = 100; public float asteroidSize = 1; public float asteroidDistance = 10; public float asteroidClipDistance = 1; private float asteroidDistanceSqr; private float asteroidClipDistanceSqr; private ParticleSystem pSystem; private int updateEvery = 5; private int counter = 0; GameObject root; void Start() { root = new GameObject(); root.transform.position = Vector3.zero; root.name = "Small Asteroid Field"; tx = transform; asteroidDistanceSqr = asteroidDistance * asteroidDistance; asteroidClipDistanceSqr = asteroidClipDistance * asteroidClipDistance; pSystem = GetComponent<ParticleSystem>(); } private void CreateStars() { for (int i = 0; i < asteroidsMax; i++) { asteroids[i] = Instantiate(prefabs[Random.Range(0, prefabs.Length - 1)]) as GameObject; asteroids[i].transform.position = Random.insideUnitSphere * asteroidDistance + tx.position; asteroids[i].transform.parent = root.transform; asteroids[i].GetComponent<Rigidbody>().angularVelocity = Random.insideUnitSphere * 2f; } } void Update() { counter++; if (asteroids[0] == null) CreateStars(); if (counter == updateEvery) { counter = 0; for (int i = 0; i < asteroidsMax; i++) { if ((asteroids[i].transform.position - tx.position).sqrMagnitude > asteroidDistanceSqr) { asteroids[i].transform.position = Random.insideUnitSphere.normalized * asteroidDistance + tx.position; asteroids[i].GetComponent<Rigidbody>().velocity = Vector3.zero; } } } } }
      
      







船を武装します



メインキャラクターの船は、6つのサスペンションポイントで武器を運ぶ機会を与えることに決めました(重武装した戦闘機には2つまたは4つのポイントは堅すぎます)。 アームのアタッチメントポイントはGameObjectsダミーを使用して設定され、サスペンションのポイント数はハードコーディングされました(はい、悔い改めました)。 同時に、サスペンションポイントは2つのタイプに分けられました-メインウェポンと補助。 基本的に1つの違いがありました-メインスロットの武器はLMBで、追加の武器では-スペースバーで有効になりました。 武器は、基本的な機能へのアクセスを提供するインターフェースを介して、新しい種類の武器を簡単に追加できるようにするという、非常に限定的な船とのやり取りを行いました。 さらに、開発時に、ストアと格納庫のインターフェイスでの外観を担当するいくつかの機能が追加されました。



インターフェース
 interface IWeapon { void Shoot(Transform target); void Refill(); int GetAmmoNum(); void SetAmmoNum(int n); Vector3 GetJointOffset(); string GetWeaponName(); string GetWeaponTitle(); string GetWeaponDesc(); int GetWeaponPrice(); Vector3 GetGUIRotation(); Vector3 GetGUIScale(); }
      
      









吊り下げられた武器を持つ船の最初のスクリーンショットの1つ



合計で、5種類の武器が製造されました。2重砲、レーザー、誘導されていないミサイル、重いホーミング魚雷、ホーミングミサイルです。



ターゲットが同じ方向に同じ速度で移動すると仮定して、精度を高めるためのホーミングミサイルがリードポイントを計算しました。 原則として、これはミサイルがかなり高い確率で動いているターゲットにヒットするのに十分でした。



ガイド付きミサイルスクリプトコード
 public class GuidedRocket : MonoBehaviour { public float selfDestructTime = 5f; public float safetyDelay = 0.5f; public float damageRadius = 6f; public int damage = 120; public float acceleration; public float steerCoef = 0.09f; float maxSpeed = 10f; public Collider col; bool armed = false; public Transform smoke; public GameObject explosion; public GameObject target; Vector3 estimatedPosition; Vector3 targetVelocity; Rigidbody targetBody; public Vector3 fwd = -Vector3.up; public Vector3 fixAngle = new Vector3(-90, 0, 0); float startSpeed; bool first = true; Rigidbody body; public LayerMask hitMask; void Start() { Invoke("SelfDestruct", selfDestructTime); Invoke("Arm", safetyDelay); body = GetComponent<Rigidbody>(); } void FixedUpdate() { if (first) { first = false; startSpeed = transform.InverseTransformDirection(GetComponent<Rigidbody>().velocity).magnitude; maxSpeed = startSpeed * 2; if (target != null) { targetBody = target.GetComponent<Rigidbody>(); } } Vector3 fwdDir = transform.TransformDirection(fwd); if (armed) { startSpeed += Time.deltaTime * acceleration; if (target != null) { Vector3 enemyPos = target.transform.position; Vector3 dir = enemyPos - transform.position; if (targetBody != null) { float distance = dir.magnitude; Vector3 tgVel = targetBody.velocity; estimatedPosition = target.transform.position + (distance / startSpeed) * tgVel; //   dir = estimatedPosition - transform.position; } else { } Quaternion targetRotation = Quaternion.LookRotation(dir) * Quaternion.Euler(fixAngle); body.MoveRotation(Quaternion.Lerp(transform.rotation, targetRotation, steerCoef)); } } body.velocity = Vector3.Lerp(body.velocity,fwdDir * startSpeed , 0.5f); } void SelfDestruct() { smoke.parent = null; smoke.gameObject.AddComponent<Autodestruction>().Set(5f); smoke.gameObject.GetComponent<ParticleSystem>().enableEmission = false; explosion.transform.parent = null; explosion.SetActive(true); Destroy(this.gameObject); } void Arm() { armed = true; col.enabled = true; } void OnTriggerEnter(Collider col) { if (armed) { Debug.Log(col.gameObject.name); foreach (Collider c in Physics.OverlapSphere(transform.position, damageRadius)) { if (c.GetComponent<Rigidbody>()) { c.GetComponent<Rigidbody>().AddExplosionForce(20, this.transform.position, damageRadius); c.GetComponent<Rigidbody>().AddTorque(new Vector3(Random.Range(-30f, 30f), Random.Range(-30f, 30f), Random.Range(-30f, 30f))); } if (c.gameObject.GetComponent<IDamageReciever>() != null) { c.gameObject.GetComponent<IDamageReciever>().DoDamage(damage); } } CancelInvoke("SelfDestruct"); SelfDestruct(); } } }
      
      







以下は、2種類の武器の動作のビデオです。 ビデオには敵の破壊の効果も示されていますが、それは非常に単純なので、別に説明しません。











インターフェース



インターフェイスに費やした時間は予想外に非常に重要であることが判明し、インターフェイスの開発には少なくとも3日かかりました。 Unity 4.6に登場したUIシステムを使用しましたが、私の意見では、それは非常に便利で、学ぶのはかなり簡単です。



インターフェイスでの作業の中で最も難しい部分はベースインターフェイスであり、非常に多くの機能を実装する必要がありました。 対話、武器の購入/販売、ホールドからのアイテムの販売、修理。 貨物/武器アイコンとして、アイコンを描画する時間が十分になかったため、直交投影のモデルが使用されました。 アイテムごとに短い説明がまとめられ、プレーヤーの前に何があるかを伝えました。





インターフェイスの最初のバージョン、カーブ、タスクタブなし



後に、インターフェースはグラフィカルに洗練され(彼のためにスプライトを手伝ってくれた人のおかげで、私自身は決してできなかったでしょう)、顕著によりきれいできれいになり始めました。





最終バージョンでは、本質は同じですが、目を楽しませてくれます。



飛行インターフェースがより簡単になりました。 プレイヤーは実際には彼とは一切対話せず、それ自体で彼はかなりミニマルです。 プログレスバーには、通常のスプライトが使用され、Unityを使用して9つの部分にカットされ、非常に簡単かつ非常にきれいに水平に引き伸ばされました。 スコープの側面に湾曲したバーがあると、すべてがより複雑になります。 軸の1つに沿ってスプライトをストレッチするだけでは機能しません。より複雑なソリューションを使用する必要があります。 単純なシェーダーは、カットオフの高さという1つのパラメーターで記述されています。 スクリプトから設定され、それに応じて、スプライトは目的の塗りつぶしで描画されました。



シェーダーコード
Shader "Custom/CrosshairShader" {

Properties {

_MainTex ("Base (RGB)", 2D) = "white" {}

_Angle ("Angle (Float)", Float) = 0

_Color ("Tint", Color) = (1.0, 0.6, 0.6, 1.0)

}



SubShader {

Tags{"Queue" = "Transparent" }

Pass {

Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM

#pragma vertex vert_img

#pragma fragment frag



#include "UnityCG.cginc"



uniform sampler2D _MainTex;

uniform float _Angle;

uniform fixed4 _Color;



float4 frag(v2f_img i) : COLOR

{

float4 result = tex2D(_MainTex, i.uv);

float angle = i.uv.y;



if(angle > _Angle)

{

result.a = 0;

}



return result*_Color;

}

ENDCG

}

}

}









フライトインターフェースのスクリーンショット:





スクリーンショットは、画面の中央に半分満たされたヘルスインジケータを示しています



相手



私の意見では、ゲームの弱点の1つです。 彼らはただ一つの戦術を持っています-プレーヤーが彼らの視野に入っていて、射撃の距離に近づいたら、プレーヤーの後に飛ぶことは、射撃を始めます。 それにもかかわらず、多くの場合、そのような敵でさえ深刻な危険をもたらします。



もちろん、上記の動作に加えて、補助機能もありました。 たとえば、プレーヤーの視界を失った彼らは、彼を見た最後のポイントまで飛んで、そこで彼を見つけられなかった、ランダムにさまよい始めた、または開始ポイントに戻った。 それでも、彼らは小惑星との衝突を時々避けたが、それは彼らがあまりに速く飛ばなかった場合だけだ。 敵のAI用の本格的なステートマシンはそのようには実装されていませんでした。AIスクリプトは0.1秒ごとにパスを開始し、次の0.1秒で敵が何をするかを決定しました。 このため、多かれ少なかれ許容できる動作を実現するために、AIの状態を記述する多くの追加変数を開始する必要がありました。





メインボスは完全に無防備であり、彼の膨大な数の手下だけに頼っていました。



クエスト



それ以前は、クエストシステムを実行したことがなかったため、どこから始めればよいのかわかりませんでした。 しかし、推論する時間やレッスンを探す時間はありませんでした。行動する必要がありました。2週目が続いていたからです! その結果、ハードコード化されたクエストのリストを備えた非常にsystemいシステムが生まれました(新しいクエストを追加するには、メインクエストスクリプトの変数にその名前を書き、クエストの数を1つ増やし、クエストが参照するベースを指定する必要があります)、ダイアログの固定数の回答オプション(3ピース)、およびレーキジャンプのその他の兆候があります。



ダイアログはxmlに保存されました。編集のために、Unityの同じ場所で簡単なエディターも作成されました。





エディターはおそらくクエストシステムの最も優れた部分です。



クエスト自体はプレハブによって作成され、プレハブはクエストの起動時にインスタンス化され、賞を受け取った後に削除されました。 曲がっていますが、うまくいきました。 クエストシステム、ダイアログの作成、およびクエスト自体はさらに3日間かかりました。 締め切りは間近に迫っていました。



その他のささいなこと



ここでは、別のセクションに値しないさまざまなチップをリストしたいと思いますが、それでもそれについてお話ししたいと思います。



多分、ハイパージャンピングの効果から始めましょう。 どのように見えるか、以下を見ることができます。







非常に簡単に実装されます。 最初にサウンドを開始し、カメラを振って、メインモデルの上に同じ船のわずかに拡大したモデルを表示しますが、「エネルギーフィールド」のテクスチャを使用して、アニメーションカーブでアニメーション化し、ジャンプの前にパーティクルフラッシュシステムを起動してメインモデルを非表示にします。 「低予算」効果のためにそれがうまくいったように思えます。



また、保存システムについて少し説明したいと思います。 奇妙なことに、彼女はゲームに参加しています。 プレーヤーの名前を含むxmlファイルはゲームフォルダーに保存され、プレーヤーの名前自体はPlayerPrefs(レジストリ内)に保存されます。 理論的には、メインメニューでプレーヤーの名前を変更することで、複数の保存を使用することもできます。 クエストが完了するたびに、保存は自動的に行われます。



非常に簡単に実装されます:SaveInfoクラスが作成され、ゲームの必要なすべての状態パラメーターが保存されます。このクラスは、標準C#ツールを使用してデータをxmlファイルにシリアル化/非シリアル化する2つのメソッドLoad()およびSave()を実装します



ロードして保存
 public void Save(string path) { var serializer = new XmlSerializer(typeof(SaveInfo)); using (var stream = new FileStream(path, FileMode.Create)) { serializer.Serialize(stream, this); stream.Close(); } } public static SaveInfo Load(string path) { var serializer = new XmlSerializer(typeof(SaveInfo)); using (var stream = new FileStream(path, FileMode.Open)) { return serializer.Deserialize(stream) as SaveInfo; } }
      
      







それだけです。他に何もする必要はありません。このクラスのフィールドを解析するだけです。



ファイルを保存
 <?xml version="1.0" encoding="windows-1251"?> <SaveInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <name>Mingebag</name> <money>150</money> <health>460</health> <maxHealth>500</maxHealth> <maxcargo>2500</maxcargo> <maxSpeed>25</maxSpeed> <acceleration>3</acceleration> <yawPitchFactor>1.99</yawPitchFactor> <rollFactor>1.98</rollFactor> <wS1Name>Machinegun</wS1Name> <wS1Num>0</wS1Num> <wS2Name>Machinegun</wS2Name> <wS2Num>0</wS2Num> <auxName> <string>GuidedLauncher</string> <string>NursLauncher</string> <string>NursLauncher</string> <string>GuidedLauncher</string> </auxName> <auxNum> <int>6</int> <int>18</int> <int>18</int> <int>9</int> </auxNum> <spawnInNextScene>false</spawnInNextScene> <spawnPositionIndex>0</spawnPositionIndex> <spawned>true</spawned> <hasActiveQuest>false</hasActiveQuest> <base1QuestNum>0</base1QuestNum> <base2QuestNum>0</base2QuestNum> <InventoryItems> <Item> <name>Equipment3</name> <title> </title> <desc>  ,   .</desc> <weight>300</weight> <quantity>1</quantity> <price>100</price> </Item> <Item> <name>Equipment2</name> <title> </title> <desc>  .    </desc> <weight>150</weight> <quantity>1</quantity> <price>100</price> </Item> <Item> <name>Debris</name> <title> </title> <desc>  .    </desc> <weight>50</weight> <quantity>1</quantity> <price>10</price> </Item> </InventoryItems> </SaveInfo>
      
      







したがって、ゲームを進めるのが面倒な場合は、保存して進行を加速したり、武器をより強力にしたりすることができます。



また、ゲームのグラフィックコンテンツの開発を手伝ってくれた開発プロセスのすぐれた善良な人々にも感謝したいと思います。 その量を考えると(数十のモデルのみ)、私はそれを時間通りに捕まえることができるとは思わず、品質について話す必要はありません。 その結果、ゲーム内のほとんどすべてのグラフィックス(2つの背景とスカイボックスを除く)は、このゲーム専用に作成されました。



まとめ



ゲームの作業が終了してから半年以上が経過しており、在庫を確認する時が来ました。



私はこのゲームで最優秀賞を獲得しませんでした。どうやらピクセルアートアドベンチャーのトレンドに参加できなかったようです。 しかし、ゲームの作業中に非常に多くの貴重な経験とスキルが得られ、追い越されたレーキからのバンプは、いくつかのことは価値がないことを長い間思い出させます。



結論:





もちろん、私の結論の多くは明白に思えるかもしれませんが、時には試してみないと理解できないでしょう。 たとえば、プロジェクトをできるだけ複雑にしたいという欲求は、初心者の開発者が犯す最も一般的な間違いの1つです。 私もこの問題を回避できませんでした。



最後に、ゲームのゲームプレイビデオを紹介します。 その上で、最終的に何が起こったかを大まかに評価できます。







この記事が何らかの形であなたの役に立つことを願っています。少なくとも、別の小さなゲームの開発の歴史を知ることは興味深いものでした。



ビルドへのリンク:

yadi.sk/d/j-v8WoyCiaNuQ



GitHubリポジトリへのリンク(コンテストがほとんど修正されなかった後の生のコード、およびすべてが急いで書かれたため、真珠はそこにいっぱいです)

github.com/TxN/TWG6-Spacesim



All Articles