プレイされる方はこちらからダウンロード

概要
- リードプログラマーとして参加
- 開発人数 10人(プロデューサー1人、ゲームデザイナー1人、サウンドデザイナー1人、グラフィックデザイナー2人、プログラマー5人)
- 東京ゲームショー2023に展示
- 古代遺跡を探索しながら突進して敵を倒す3D爽快ロックオンアクションゲーム
開発環境
Unity2021.3.13f1/Visual Studio 2019/C#/Git/Sourcetree
開発期間
2022年10月~2023年9月
詳細
本作品は、1年生の時から続いたゲーム制作を学ぶ授業にて、3年生後期から制作を開始し、東京ゲームショー2023に展示した作品です。Unity、C#のどちらも使用し始めてから半年の状態でリードプログラマーというチーム内のプログラマーを統括する役職で制作に携わっていたため、始めはコミュニケーション能力やプログラミングの技術が未熟であったこともあり、かなり苦労しました。しかし、1年にも及ぶ制作で誰一人欠けることなく完走できたことで良い経験になったと考えています。
学内で行われた3度のオープンキャンパスでの展示や2週間に一度行われた講師に向けた進捗報告にて、フィードバックをもらうことで改善する点を見つけ、短期間で改善することを繰り返すことでより良い作品になったと感じています。
主な担当箇所
画面内の敵のロックオン
始めは、PlayerにBoxColliderを付けて、敵を判定していたのですが、この手法では遮蔽物に隠れている敵にも突進できてしまうなどの問題がありました。そのため、カメラにRendererが写っているか確認すると同時に敵にRayを出し、遮蔽物に隠れていない敵を判別し、ロックオンしています。また、Playerからの距離によってRayを出すか出さないか決めているため使用するRayの本数を減らし、処理を軽量化しています。
Script
IsRendererd.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class IsRendered : MonoBehaviour
{
//敵にこのスクリプト付けてください
//検出対象のRenderer
[SerializeField] public MeshRenderer targetRenderer;
//突進目標のRenderer
[SerializeField] public MeshRenderer StatueRenderer;
//近接敵静止状態のRenderer
[SerializeField] MeshRenderer StatueRenderer_01;
//近接敵動いている状態のRenderer
[SerializeField] MeshRenderer StatueRenderer_02;
//現在のCamera
[SerializeField] Camera cam;
//EnemyLayerに設定してください
[SerializeField] LayerMask layerMask;
//近接敵静止状態のオブジェクト
[SerializeField] GameObject enemy_01;
//近接敵動いている状態のオブジェクト
[SerializeField] GameObject enemy_02;
//可視化判定
public bool visible;
//検出メソッド用に取得
SingleTarget st;
//突進座標設定用に取得
target t;
void Start()
{
st = GameObject.FindGameObjectWithTag("Manager").GetComponent<SingleTarget>();
//Bossのみ大きすぎるため検出用Rendererを胴体部に限定
if (this.gameObject.CompareTag("BOSS"))
{
GameObject first = this.transform.GetChild(3).gameObject;
targetRenderer = first.transform.GetChild(0).GetComponent<MeshRenderer>();
}
else
{
targetRenderer = this.gameObject.GetComponent<MeshRenderer>();
}
//近接敵は中心がずれているため、突進目標のRendererを胴体部に限定
if(this.gameObject.CompareTag("Statue"))
{
GameObject first = this.transform.GetChild(5).gameObject;
enemy_01 = first.transform.GetChild(0).gameObject;
enemy_02 = first.transform.GetChild(1).gameObject;
StatueRenderer_01 = enemy_01.transform.GetChild(0).gameObject.GetComponent<MeshRenderer>();
StatueRenderer_02 = enemy_02.transform.GetChild(0).gameObject.GetComponent<MeshRenderer>();
}
cam = Camera.main;
t = GameObject.FindGameObjectWithTag("Player").GetComponent<target>();
}
void Update()
{
cam = Camera.main;
//近接敵の切り替えに応じて、Rendererを変更
if (this.gameObject.CompareTag("Statue"))
{
if (enemy_01.activeSelf == true)
{
StatueRenderer = StatueRenderer_01;
}
if (enemy_02.activeSelf == true)
{
StatueRenderer = StatueRenderer_02;
}
}
//可視化判定
st.IsVisibleFromWall(targetRenderer, StatueRenderer, cam, layerMask, this.gameObject);
//Boss、近接敵の場合のみ突進目標を変更
if(this.gameObject.CompareTag("Statue") && t.TargetStatue == this.gameObject)
{
t.StatuePos2 = StatueRenderer.bounds.center;
}
if (this.gameObject.CompareTag("BOSS") && t.TargetBoss == this.gameObject)
{
t.BossPos = StatueRenderer.bounds.center;
}
}
}
Singletarget.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SingleTarget : MonoBehaviour
{
MeshRenderer Player;
single s;
targetChange tc;
IsRendered R;
Combo c;
multipleTarget mt;
target t;
// Start is called before the first frame update
void Start()
{
Player = GameObject.FindGameObjectWithTag("Player").GetComponent<MeshRenderer>();
s = this.gameObject.GetComponent<single>();
tc = this.gameObject.GetComponent<targetChange>();
c = Player.GetComponent<Combo>();
t = Player.GetComponent<target>();
}
// Update is called once per frame
void Update()
{
}
//カメラの描画範囲内かつ遮蔽物に隠れていない場合targetListに追加
public void IsVisibleFromWall(MeshRenderer renderer,MeshRenderer StatueRenderer , Camera camera, LayerMask layerMask, GameObject targetObject)
{
//カメラの描画範囲取得
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(camera);
//検出に使用するRenderer
Bounds bounds = renderer.bounds;
//対象オブジェクトの座標
Vector3 targetPosition;
targetPosition = StatueRenderer.bounds.center;
//対象とPlayerの距離計算
float dis = Vector3.Distance(Player.bounds.center, targetPosition) + Vector3.Distance(camera.transform.position, Player.bounds.center);
//カメラの描画範囲にRendererがあり、Playerとの距離が120f以内の場合のみ遮蔽物判定を行う
if(GeometryUtility.TestPlanesAABB(planes, bounds) == true && dis < 120f)
{
RaycastHit hit;
//Rayの開始位置
Vector3 origin;
//カメラが特殊演出中出ない場合は、Rayの開始位置を変更
if (!c.SpecialMode || !mt.ChangeCamera)
{
origin = camera.transform.position + new Vector3(0.0f, 5.0f, 0.0f);
}
else
{
origin = camera.transform.position;
}
//Rayの方向
Vector3 rayDirection = targetPosition - origin;
//Rayの最大距離
float rayDistance = 70f;
Physics.Raycast(origin , rayDirection, out hit, rayDistance, layerMask);
//Rayのデバッグ
Debug.DrawRay(origin, rayDirection * rayDistance, Color.red, 0.1f, false);
//可視化可能であることを敵のIsRenderdへ
R = targetObject.GetComponent<IsRendered>();
R.visible = true;
//Rayが対象オブジェクトにヒットした時のみtargetListへ追加
if(hit.collider.gameObject == targetObject)
{
if (!s.targetList.Contains(targetObject))
{
//敵の種類に応じて処理を変更
if(targetObject.GetComponent<BeamHPManager>() != null)
{
if(!targetObject.GetComponent<BeamHPManager>().Boss && !targetObject.GetComponent<BeamHPManager>().isDefeat)
{
s.targetList.Add(targetObject);
s.Sort = true;
tc.Change = true;
}
}
else if (targetObject.GetComponent<StatueHPManager>() != null)
{
if (!targetObject.GetComponent<StatueHPManager>().Boss)
{
s.targetList.Add(targetObject);
s.Sort = true;
tc.Change = true;
}
}
else if (targetObject.CompareTag("BOSS"))
{
s.targetList.Add(targetObject);
s.Sort = true;
tc.Change = true;
}
}
}
else
{
//Playerが突進状態出なく、対象が遮蔽物に隠れた場合は、targetListから削除
if (!t.isMoving)
{
s.targetList.Remove(targetObject);
R.visible = false;
}
}
}
else
{
//Playerが突進状態出なく、対象がカメラの描画範囲内からいなくなった場合は、targetListから削除
if (!t.isMoving)
{
s.targetList.Remove(targetObject);
R.visible = false;
}
}
}
}
single.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class single : MonoBehaviour
{
//突進可能な敵格納用List
[SerializeField]
public List<GameObject> targetList;
private target ta;
[SerializeField]
public GameObject Playerobj;
//ソートするタイミングを図る際に使用
public bool Sort = false;
bool Change;
void Start()
{
Playerobj = GameObject.FindGameObjectWithTag("Player");
ta = GameObject.FindGameObjectWithTag("Player").GetComponent<target>();
}
void Update()
{
if (Sort == true)
{
//敵の種類に応じて、targetに突進可能な敵を受け渡す
if (targetList[0].CompareTag("Statue"))
{
ta.TargetStatue = targetList[0];
ta.TargetBeam = null;
ta.TargetBoss = null;
ta.isTarget_Statue = true;
ta.isTarget_Beam = false;
ta.isTarget_Boss = false;
}
if (targetList[0].CompareTag("Beam"))
{
ta.TargetBeam = targetList[0];
ta.TargetStatue = null;
ta.TargetBoss = null;
ta.isTarget_Beam = true;
ta.isTarget_Statue = false;
ta.isTarget_Boss = false;
}
if (targetList[0].CompareTag("BOSS"))
{
ta.TargetBoss = targetList[0];
ta.TargetStatue = null;
ta.TargetBeam = null;
ta.isTarget_Boss = true;
ta.isTarget_Statue = false;
ta.isTarget_Beam = false;
}
}
//List内の要素が0の場合、ソート可能に
if (!(targetList.Count > 0))
{
Sort = true;
}
//Listの先頭のオブジェクトが消えた場合、ソート
if (targetList[0] == null || targetList[0].activeSelf == false|| !ta.isMoving)
{
SortVectorList();
}
}
//targetListの要素を削除
public void ListClear()
{
targetList.Clear(); //Listすべての要素を削除
}
//targetListをPlayerに近い順にソート
public void SortVectorList()
{
Vector3 characterPosition = Playerobj.transform.position;
targetList.RemoveAll(item => item == null || !item.activeSelf);
targetList.Sort((a, b) => Vector3.Distance(a.transform.position, characterPosition).CompareTo(Vector3.Distance(b.transform.position, characterPosition)));
}
}
単体の敵への突進
Colliderによる判定だと敵に既に触れている状態で攻撃状態に遷移した場合に敵にダメージを与えられません。しかし、MoveTowardを使用し、目標座標に到達した場合に敵にダメージを与えるという処理にしたことでこの問題を解決しました。
必殺技
カメラが切り替わった際に、画面の中心を強調するためにポストエフェクトを使用して中心への注目度を上げています。また、中ボスやボスを倒しやすくするために、必殺技の移動の最大距離の座標にオブジェクトを配置して、その座標に向かってRayを出しています。そして、Rayが中ボスに当たった時に、画面の中心がボスや中ボスの中心に固定され、狙いやすくなります。また、何か操作をすることで画面固定は解除されます。そして、中ボスやボスに必殺技が当たった際は、移動後にその場で処理が終わりますが、床や壁に当たった際に移動が止まることは必殺技として不自然だと考えたため、床や壁に向かって移動した場合は、反射するように調整しました。
Script
target.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
[System.Serializable]
public class Data
{
public Vector3 position;
// その他のデータを追加
}
public class target : MonoBehaviour
{
//突進対象の近接敵
public GameObject TargetStatue;
//突進対象のビーム敵
public GameObject TargetBeam;
//突進対象のBoss
public GameObject TargetBoss;
//突進の速度
public float RushSpeed = 0f;
//突進対象の近接敵の座標
public Vector3 StatuePos2;
//突進対象のビーム敵の座標
public Vector3 BeamPos;
//突進対象のBossの座標
public Vector3 BossPos;
public Rigidbody rb;
//敵の種類に応じて突進中かターゲット中か判定
public bool isTarget_Statue = false;
public bool isTarget_Beam = false;
public bool ismove_Statue = false;
public bool ismove_Beam = false;
public bool isTarget_Boss = false;
public bool ismove_Boss = false;
public bool clear = false;
//当たった時の重力変更と当たった時のはね
public float jump = 3f;
public float JumpGravityY = -10;
public Vector3 surfacePoint;
private single si;
public List<Data> dataList = new List<Data>();
public Vector2 targetPosition;
//攻撃時のフラグ
public bool Attack;
#region 突進による吹っ飛ばし
public static target instance;
public bool isAtacked = false;
public void Awake()
{
if (instance == null)
{
instance = this;
}
}
#endregion
// PlayerSoundsスクリプト--------村岡追加--------
private PlayerSounds ps;
private Soundtest st;
private bool isPlaySPChargeSound;
[SerializeField]
private float center;
public bool jumpwait = false;
public Vector3 nowPos;
public float backKnockBackForce;
public float upKnockBackForce;
public float knockBackPower;
multipleTarget mt;
multiplePlayer mp;
Combo c;
public bool SpecialAttack;
public bool SpecialAtStart;
public Vector3 SpecialPosition;
public Vector3 FinalSpecialPosition;
public bool isMoving = false;
[SerializeField] float slowMotionScale = 0.2f;
public Vector3 FirstPosition;
Vector3 SurfaceNormal;
[SerializeField] GameObject SpecialEffect;
float originalTimeScale = 1.0f;
int enemyLayerMask;
int BossLayerMask;
public Vector3 SingleMovePosition;
public GameObject Target;
// Start is called before the first frame update
void Start()
{
rb = GetComponent<Rigidbody>();
//TargetImage.GetComponent<Image>().enabled = false;
mt = GameObject.FindGameObjectWithTag("Manager").GetComponent<multipleTarget>();
mp = GameObject.FindGameObjectWithTag("Player").GetComponent<multiplePlayer>();
c = this.gameObject.GetComponent<Combo>();
// PlayerSoundsスクリプト取得--------M追加--------
ps = GetComponent<PlayerSounds>();
st = GameObject.Find("SEPlayer").GetComponent<Soundtest>();// ここは一番下の行にしてください
SpecialEffect.SetActive(false);
originalTimeScale = 1.0f;
enemyLayerMask = 1 << LayerMask.NameToLayer("Enemy");
BossLayerMask = 1 << LayerMask.NameToLayer("Boss");
}
// Update is called once per frame
void Update()
{
//突進可能なオブジェクトがnullでない場合は、敵に応じてflagを変更
if (TargetBeam != null)
{
isTarget_Beam = true;
isTarget_Statue = false;
isTarget_Boss = false;
}
else if (TargetStatue != null)
{
isTarget_Beam = false;
isTarget_Statue = true;
isTarget_Boss = false;
}
else if (TargetBoss != null)
{
isTarget_Beam = false;
isTarget_Statue = false;
isTarget_Boss = true;
}
//敵に応じて突進の処理を変更
if ((TargetStatue == null || !TargetStatue.activeSelf) && ismove_Statue)
{
ismove_Statue = false;
isMoving = false;
isAtacked = false;
Attack = false;
}
else if ((TargetBeam == null || !TargetBeam.activeSelf)&& ismove_Beam)
{
ismove_Beam = false;
isMoving = false;
isAtacked = false;
Attack = false;
}
//近接敵への突進処理
if (isTarget_Statue == true && !mt.multiple)
{
if (Input.GetMouseButtonDown(0) && !isMoving)
{
ismove_Statue = true;
isMoving = true;
SingleMovePosition = StatuePos2;
Target = TargetStatue;
// プレイヤーの突進音をON--------村岡追加--------
ps.isPlayRushSound = true;
}
}
//ビーム敵への突進処理
if (isTarget_Beam == true)
{
BeamPos = TargetBeam.transform.position;
if (Input.GetMouseButtonDown(0) && !isMoving)
{
ismove_Beam = true;
isMoving = true;
SingleMovePosition = TargetBeam.transform.position;
Target = TargetBeam;
// プレイヤーの突進音をON--------M追加--------
ps.isPlayRushSound = true;
}
}
//Bossへの突進処理
if (isTarget_Boss == true)
{
if (Input.GetMouseButtonDown(0))
{
ismove_Boss = true;
isMoving = true;
SingleMovePosition = BossPos;
Target = TargetBoss;
// プレイヤーの突進音をON--------M追加--------
ps.isPlayRushSound = true;
}
}
//必殺技演出
if (c.Special == true && isPlaySPChargeSound == false)
{
st.SE_SPChargePlayer();
isPlaySPChargeSound = true;
}
if (c.Special == false && isPlaySPChargeSound == true)
{
isPlaySPChargeSound = false;
}
//近接敵への突進、突進完了後の処理
if(ismove_Statue)
{
transform.position = Vector3.MoveTowards(this.transform.position, SingleMovePosition, RushSpeed * Time.deltaTime);
rb.isKinematic = true;
Attack = true;
if ((Mathf.Approximately(this.transform.position.x, SingleMovePosition.x) && Mathf.Approximately(this.transform.position.y, SingleMovePosition.y) && Mathf.Approximately(this.transform.position.z, SingleMovePosition.z)))
{
if (Target != null && Target.activeSelf)
{
//対象にダメージを与える
Target.GetComponent<StatueHPManager>().SingleDamage();
}
isTarget_Statue = false;
ismove_Statue = false;
isMoving = false;
rb.isKinematic = false;
Attack = false;
TargetStatue = null;
// プレイヤーの攻撃をした時の音をON(アニメーションが出来次第移動)--------M追加--------
ps.isPlayAttackSound = true;
// プレイヤーの攻撃が当たった時の音をON--------M追加--------
//ps.isPlayAttackHitSound = true;
}
}
//ビーム敵への突進、突進完了後の処理
if (ismove_Beam)
{
transform.position = Vector3.MoveTowards(this.transform.position, SingleMovePosition, RushSpeed * Time.deltaTime);
rb.isKinematic = true;
Attack = true;
if ((Mathf.Approximately(this.transform.position.x, SingleMovePosition.x) && Mathf.Approximately(this.transform.position.y, SingleMovePosition.y) && Mathf.Approximately(this.transform.position.z, SingleMovePosition.z)))
{
if (Target != null && Target.activeSelf)
{
//対象にダメージを与える
Target.GetComponent<BeamHPManager>().SingleDamage();
}
ismove_Beam = false;
isMoving = false;
rb.isKinematic = false;
Attack = false;
isTarget_Beam = false;
TargetBeam = null;
// プレイヤーの攻撃をした時の音をON(アニメーションが出来次第移動)--------村岡追加--------
ps.isPlayAttackSound = true;
// プレイヤーの攻撃が当たった時の音をON--------村岡追加--------
//ps.isPlayAttackHitSound = true;
}
}
//Bossへの突進、突進完了後の処理
if (ismove_Boss)
{
transform.position = Vector3.MoveTowards(this.transform.position, SingleMovePosition, RushSpeed * Time.deltaTime);
rb.isKinematic = true;
Attack = true;
if ((Mathf.Approximately(this.transform.position.x, SingleMovePosition.x) && Mathf.Approximately(this.transform.position.y, SingleMovePosition.y) && Mathf.Approximately(this.transform.position.z, SingleMovePosition.z)))
{
if (Target != null && Target.activeSelf)
{
//対象へダメージを与える
Target.GetComponent<Clear>().SingleDamage();
}
ismove_Boss = false;
isMoving = false;
rb.isKinematic = false;
Attack = false;
isTarget_Boss = false;
TargetBoss = null;
// プレイヤーの攻撃をした時の音をON(アニメーションが出来次第移動)--------村岡追加--------
ps.isPlayAttackSound = true;
// プレイヤーの攻撃が当たった時の音をON--------村岡追加--------
//ps.isPlayAttackHitSound = true;
}
}
}
//突進後のノックバック
public void KnockBack(Collision collision)
{
nowPos = this.transform.position;
var boundVec = (nowPos - collision.transform.position);
boundVec.y = 0.0f;
boundVec = boundVec.normalized * backKnockBackForce;
boundVec.y = 1.0f * upKnockBackForce;
boundVec = boundVec.normalized;
rb.velocity = Vector3.zero;
rb.AddForce(boundVec * knockBackPower, ForceMode.Impulse);
}
//敵に近づき過ぎた場合に後ろに下がる
private void GetBack()
{
Vector3 First = new Vector3(FirstPosition.x, this.transform.position.y, FirstPosition.z);
Vector3 movePoint = new Vector3(surfacePoint.x, this.transform.position.y, surfacePoint.z);
float moveDistance = (movePoint - First).magnitude;
float moveAmount = 5.0f;
Vector3 moveDirection = (First - movePoint).normalized;
this.transform.position = Vector3.Lerp(this.transform.position, this.transform.position + moveDirection * moveAmount,Time.deltaTime * RushSpeed);
}
//ノックバック処理
public void SingleKnockBack(float MoveDistance, float updistance)
{
FirstPosition.y = SingleMovePosition.y;
Vector3 Direction = (Target.transform.position - FirstPosition).normalized;
Vector3 upOffset = new Vector3(0f, updistance, 0f);
Vector3 newPosition = transform.position - Direction * MoveDistance + upOffset;
transform.position = Vector3.Lerp(transform.position, newPosition, 2f * Time.deltaTime);
rb.velocity = Vector3.zero;
Debug.Log("Back");
}
private void OnCollisionEnter(Collision other)
{
if ((other.gameObject.tag == "Statue" || other.gameObject.tag == "Beam") && SpecialAttack)
{
//必殺技使用時のSE
st.SE_SuperAttackPlayer();
}
}
//敵の防御力がPlayerの攻撃力よりも高かった場合のノックバック処理
public void DenfensiveKnockBack()
{
Vector3 incidentVector = (FirstPosition - SingleMovePosition).normalized;
float KnockBackDistance = 15f;
Vector3 moveDirection = transform.position + incidentVector * KnockBackDistance;
rb.velocity = Vector3.zero;
this.transform.position = Vector3.Lerp(this.transform.position,moveDirection, Time.deltaTime * RushSpeed);
}
IEnumerator ResetCollisionFlag()
{
yield return new WaitForSeconds(1f);
jumpwait = false;
}
public void AddData(Data data)
{
dataList.Add(data);
}
//必殺技発動コルーチン
public IEnumerator DelayedStart(float delayInSeconds)
{
SpecialAtStart = true;
rb.useGravity = false;
rb.velocity = Vector3.zero;
//必殺技の開始地点
Vector3 FirstPoint = transform.position;
//必殺技発動前のスローモーションの秒数
Time.timeScale = slowMotionScale;
//必殺技専用エフェクト出現
SpecialEffect.SetActive(true);
transform.LookAt(SpecialPosition);
yield return new WaitForSecondsRealtime(delayInSeconds);
//必殺技処理開始
// プレイヤーの突進音をON--------M追加--------
ps.isPlayRushSound = true;
//時間を元に戻す
Time.timeScale = originalTimeScale;
SpecialEffect.SetActive(false);
SpecialAttack = true;
rb.isKinematic = false;
if (!isMoving)
{
int layerMask = 1 << LayerMask.NameToLayer("Default");
int enemyLayerMask = 1 << LayerMask.NameToLayer("Boss");
Ray ray = new Ray(transform.position, SpecialPosition - transform.position);
RaycastHit hit;
//突進完了位置を事前に計算し、Boss、Variantの場合は、衝突した地点で停止、それ以外の場合は反射
if (Physics.Raycast(ray, out hit, Vector3.Distance(transform.position, SpecialPosition), (layerMask | enemyLayerMask)))
{
if ((layerMask & (1 << hit.collider.gameObject.layer)) != 0)
{
Vector3 normal = hit.normal;
// 法線ベクトルの向きによって壁か床かを判別する
if (Mathf.Abs(normal.y) > Mathf.Abs(normal.x) && Mathf.Abs(normal.y) > Mathf.Abs(normal.z))
{
// 法線ベクトルが上向き(床の法線ベクトル)の場合は床と判定
// 床に対する処理を行う
// 衝突地点を取得
Vector3 collisionPoint = hit.point;
Debug.Log("床");
FinalSpecialPosition = collisionPoint;
// 新しい位置にTweenアニメーションを設定(床に移動)
transform.DOMove(collisionPoint, 0.2f)
.SetEase(Ease.Linear)
.OnStart(() =>
{
isMoving = true;
})
.OnComplete(() =>
{
// 床への移動が完了したら、元々移動していた方向に床と水平な角度で移動
Vector3 incidentVector = (SpecialPosition - FirstPoint).normalized;
Vector3 floorNormal = hit.normal;
Vector3 reflectionVector = Vector3.Reflect(incidentVector, floorNormal);
Vector3 moveDirection = Vector3.Cross(Vector3.Cross(incidentVector, floorNormal), floorNormal).normalized;
// 移動距離を計算
float remainingDistance = 15f;
// 新しい位置を計算
Vector3 newPosition = collisionPoint + reflectionVector * remainingDistance;
isMoving = false;
SpecialAttack = false;
SpecialAtStart = false;
rb.useGravity = true;
// 新しい位置にTweenアニメーションを設定(床と水平な角度で移動)
rb.velocity = Vector3.zero;
this.transform.position = Vector3.Lerp(this.transform.position, newPosition, Time.deltaTime * RushSpeed);
});
}
else
{
// 法線ベクトルが水平方向(壁の法線ベクトル)の場合は壁と判定
// 壁に対する処理を行う
// 壁の法線ベクトルを取得
Vector3 wallNormal = hit.normal;
Debug.Log("壁");
FinalSpecialPosition = hit.point;
// 新しい位置にTweenアニメーションを設定(壁に移動)
transform.DOMove(hit.point, 0.2f)
.SetEase(Ease.Linear)
.OnStart(() =>
{
isMoving = true;
})
.OnComplete(() =>
{
// 壁への移動が完了したら、反射を実行
Vector3 incidentVector = (SpecialPosition - FirstPoint).normalized;
float remainingDistance = 15f;
Vector3 reflectionVector = Vector3.Reflect(incidentVector, wallNormal);
Vector3 newPosition = hit.point + reflectionVector * remainingDistance;
isMoving = false;
SpecialAttack = false;
rb.isKinematic = false;
SpecialAtStart = false;
rb.useGravity = true;
// 新しい位置にTweenアニメーションを設定(反射)
rb.velocity = Vector3.zero;
this.transform.position = Vector3.Lerp(this.transform.position, newPosition, Time.deltaTime * RushSpeed);
});
}
}
else if (hit.collider.CompareTag("BOSS") || hit.collider.gameObject.name.Contains("Variant"))
{
FinalSpecialPosition = hit.collider.gameObject.GetComponent<IsRendered>().StatueRenderer.bounds.center;
transform.DOMove(hit.collider.gameObject.GetComponent<IsRendered>().StatueRenderer.bounds.center, 0.3f)
.SetEase(Ease.Linear)
.OnComplete(() =>
{
isMoving = false;
SpecialAttack = false;
SpecialAtStart = false;
rb.useGravity = true;
Vector3 incidentVector = (SpecialPosition - FirstPoint).normalized;
float remainingDistance = 15f;
Vector3 newPosition = hit.point + incidentVector * remainingDistance;
this.transform.position = Vector3.Lerp(this.transform.position, newPosition, Time.deltaTime * RushSpeed);
});
}
}
else
{
FinalSpecialPosition = SpecialPosition;
// 衝突しない場合は通常のTweenアニメーションを実行
transform.DOMove(SpecialPosition, 0.3f)
.SetEase(Ease.Linear)
.OnComplete(() =>
{
isMoving = false;
SpecialAttack = false;
SpecialAtStart = false;
rb.useGravity = true;
});
}
}
}
}
複数ターゲット
Update内に移動処理を書くことでも実現できるのですが、移動中もボタンを連打してしまう方が多くいたため、単体への突進が始まってしまう不具合が出てしまいました。そのため、コルーチンを使用して、移動を実現しました。また、単体攻撃と複数ターゲットでbool変数を分けることで、バグを減らしました。
Script
multipleTarget.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;
public class multipleTarget : MonoBehaviour
{
single s;
multiplePlayer mp;
[SerializeField]
public List<GameObject> multipleTargetObject;//突進の対象
[SerializeField]
float radian;
[SerializeField]
move1_ver2 m2;
[SerializeField]
CinemachineVirtualCamera TargetCamera;//特殊カメラ
[SerializeField]
GameObject MultitargetUI;//複数ターゲット用UI
public bool target;
public bool multiple;
public bool ChangeCamera;
public Animator Playerani;
Rigidbody rb;
Combo c;
[SerializeField] GameObject P;
Soundtest st;// M追加
// Start is called before the first frame update
void Start()
{
s = GameObject.FindGameObjectWithTag("Manager").GetComponent<single>();
mp = GameObject.FindGameObjectWithTag("Player").GetComponent<multiplePlayer>();
target = false;
TargetCamera.Priority = 1;
MultitargetUI.SetActive(false);
rb = GameObject.FindGameObjectWithTag("Player").GetComponent<Rigidbody>();
c = GameObject.FindGameObjectWithTag("Player").GetComponent<Combo>();
st = GameObject.Find("SEPlayer").GetComponent<Soundtest>();// M追加
}
// Update is called once per frame
void Update()
{
//複数ターゲット、必殺技発動時にカメラ切り替え
if (ChangeCamera)
{
TargetCamera.Priority = 20;
m2.enabled = false;
Playerani.enabled = false;
MultitargetUI.SetActive(true);
rb.isKinematic = true;
P.SetActive(false);
}
if(!ChangeCamera && !c.SpecialMode)
{
TargetCamera.Priority = 1;
m2.enabled = true;
Playerani.enabled = true;
MultitargetUI.SetActive(false);
rb.isKinematic = false;
P.SetActive(true);
}
//複数ターゲットの対象を決める
if ( target && s.targetList.Count > 1)
{
multiple = true;
ChangeCamera = true;
float Distance;
Vector3 Center = new Vector3(Screen.width / 2f, Screen.height / 2f, 0f);
foreach (GameObject obj in s.targetList)
{
Vector3 TargetPosition = RectTransformUtility.WorldToScreenPoint(Camera.main, obj.transform.position);
Distance = Vector3.Distance(TargetPosition, Center);
Debug.Log(Distance);
if (!multipleTargetObject.Contains(obj) && Distance < radian && multipleTargetObject.Count < 7)
{
multipleTargetObject.Add(obj);
Rigidbody rB = obj.GetComponent<Rigidbody>();
// RigidbodyのX軸の位置制約を設定(trueで制約をかける、falseで解除)
rB.constraints = rB.constraints | RigidbodyConstraints.FreezePositionX;
// RigidbodyのY軸の位置制約を設定
rB.constraints = rB.constraints | RigidbodyConstraints.FreezePositionY;
// RigidbodyのZ軸の位置制約を設定
rB.constraints = rB.constraints | RigidbodyConstraints.FreezePositionZ;
st.SE_TargetLockedPlayer();// M追加
}
}
}
//ターゲットが1体以下の場合は、複数ターゲットを中止
if ((Input.GetKeyUp("joystick button 7") || Input.GetKeyUp("joystick button 0") || Input.GetMouseButtonUp(1)) && multipleTargetObject.Count < 2)
{
target = false;
ChangeCamera = false;
multipleTargetObject.Clear();
}
}
}
multiplePlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
public class multiplePlayer : MonoBehaviour
{
multipleTarget mt;
target t;
[SerializeField]
float RushSpeed;
single s;
public bool At;
private PlayerSounds ps;
private int next_i = 0;
Rigidbody rb;
Vector3 direction;
// Start is called before the first frame update
void Start()
{
mt = GameObject.FindGameObjectWithTag("Manager").GetComponent<multipleTarget>();
s = GameObject.FindGameObjectWithTag("Manager").GetComponent<single>();
At = false;
t = gameObject.GetComponent<target>();
rb = GetComponent<Rigidbody>();
ps = GetComponent<PlayerSounds>();
}
// Update is called once per frame
void Update()
{
if (At && mt.multipleTargetObject.Count > 1)
{
MoveTarget();
t.isMoving = true;
}
}
private void MoveTarget()
{
// 移動処理のループはUpdate内ではなく、コルーチンを使用
StartCoroutine(MoveTargetsCoroutine());
}
//複数ターゲット
private IEnumerator MoveTargetsCoroutine()
{
Vector3 lastTargetPosition = Vector3.zero; // 最後の目標の初期位置
for (int i = 0; i < mt.multipleTargetObject.Count; i++)
{
GameObject targetObject = mt.multipleTargetObject[i];
if (targetObject == null)
{
continue;
}
Vector3 targetPosition = mt.multipleTargetObject[i].GetComponent<IsRendered>().StatueRenderer.bounds.center;
direction = lastTargetPosition - targetPosition;
Tween moveTween = transform.DOMove(targetPosition, RushSpeed).SetEase(Ease.Linear);
if (i == next_i)
{
ps.isPlayAttackHitSound = true;
next_i++;
}
yield return new WaitForSeconds(0.5f);
// 最後の目標座標を更新
lastTargetPosition = targetPosition;
}
// すべてのターゲットに到達したらリセットします。
mt.multipleTargetObject.Clear();
mt.multiple = false;
s.targetList.Clear();
At = false;
next_i = 0;
t.isMoving = false;
//ノックバック
Vector3 incidentVector = direction.normalized;
float remainingDistance = 10f;
Vector3 newPosition = this.transform.position + incidentVector * remainingDistance;
this.transform.position = Vector3.Lerp(this.transform.position, newPosition, Time.deltaTime * RushSpeed);
}
}
multipleCameraMove.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;
public class MultipleCameraMove : MonoBehaviour
{
public CinemachineVirtualCamera virtualCamera;
public CinemachinePOV pov;
public Vector3 newInitialOrientation;
public GameObject MultiObject;
multipleTarget mt;
int ChangeCount;
Combo c;
target t;
[SerializeField] GameObject main;
[SerializeField] float Stoptime;
private void Start()
{
// CinemachineVirtualCameraからCinemachineComposerを取得
pov = virtualCamera.GetCinemachineComponent<CinemachinePOV>();
mt = GameObject.FindGameObjectWithTag("Manager").GetComponent<multipleTarget>();
c = GameObject.FindGameObjectWithTag("Player").GetComponent<Combo>();
t = GameObject.FindGameObjectWithTag("Player").GetComponent<target>();
}
private void Update()
{
if ((mt.ChangeCamera || c.SpecialMode)&& ChangeCount == 0)
{
StartCoroutine(CameraMoveStop(Stoptime));
}
if(!mt.ChangeCamera && !c.SpecialMode)
{
ChangeCount = 0;
}
if((mt.ChangeCamera || c.SpecialMode) && ChangeCount > 0)
{
if (Input.GetAxis("Horizontal") != 0 || Input.GetAxis("Vertical") != 0)
{
pov.m_HorizontalAxis.m_InputAxisName = "Horizontal";
pov.m_VerticalAxis.m_InputAxisName = "Vertical";
}
else
{
// より低い優先度のInputを設定
pov.m_HorizontalAxis.m_InputAxisName = "Mouse X";
pov.m_VerticalAxis.m_InputAxisName = "Mouse Y";
}
}
if(c.SpecialMode)
{
if(MultiObject.GetComponent<MultpleFirstLookAt>().LookAtBoss)
{
float verticalRotation = MultiObject.transform.rotation.eulerAngles.x;
float horizontalRotation = MultiObject.transform.rotation.eulerAngles.y;
pov.m_VerticalAxis.Value = verticalRotation;
pov.m_HorizontalAxis.Value = horizontalRotation;
}
}
}
private IEnumerator CameraMoveStop(float Stop)
{
float verticalRotation = main.transform.rotation.eulerAngles.x;
float horizontalRotation = main.transform.rotation.eulerAngles.y;
pov.m_VerticalAxis.Value = verticalRotation;
pov.m_HorizontalAxis.Value = horizontalRotation;
pov.m_HorizontalAxis.m_InputAxisName = "";
pov.m_VerticalAxis.m_InputAxisName = "";
yield return new WaitForSecondsRealtime(Stop);
ChangeCount++;
}
}


リスポーン地点の管理
Scene上からPlayerがリスポーンする座標を決める際に、デザイナーの方が確認しやすいように、リスポーン地点の座標やColliderをGizmoを使用して、可視化しました。また、リスポーン地点に使われるCubeのBoxColliderの中心をずらす、大きさを可変にすることをしました。その状態もGizmoで可視化されており、より便利になっています。
Script
Respawn.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
//リスポーン地点の座標、Colliderのサイズ、中心のデータ
[System.Serializable]
public class MyData
{
public List<Vector3> CheckPoints = new List<Vector3>();
public List<Vector3> colliderSizes = new List<Vector3>();
public List<Vector3> colliderCenter = new List<Vector3>();
public List<Vector3> LookAtPoint = new List<Vector3>();
public List<Vector3> LookAtSizes = new List<Vector3>();
public List<Vector3> LookAtCenter = new List<Vector3>();
public List<Vector3> DestroyEnemy = new List<Vector3>();
public List<Vector3> DestroyEnemySizes = new List<Vector3>();
public List<Vector3> DestroyEnemyCenter = new List<Vector3>();
}
public class Respawn : MonoBehaviour
{
public MyData data;
public GameObject CheckPoints;
private static GUIStyle labelStyle = new GUIStyle();
int i = 0;
GameObject player;
target t;
public GameObject LookAtPoint;
public bool LAP = false;
RespawnManager R;
[SerializeField]
public List<GameObject> LookAtList;
private static int idCounter = 1;
//kasuga
public GameObject DestroyEnemy;
[SerializeField]
public List<GameObject> DestroyEnemyList;
public bool DEP = false;
public int areaon = 0;
public GameObject desenobj;
// Start is called before the first frame update
void Start()
{
//リスポーン地点に設定されたColliderのCubeを生成
for (int i = 0; i < data.CheckPoints.Count; i++)
{
GameObject obj = Instantiate(CheckPoints,data.CheckPoints[i] , Quaternion.identity);
BoxCollider boxCollider = obj.GetComponent<BoxCollider>();
boxCollider.size = data.colliderSizes[i];
boxCollider.center = data.colliderCenter[i];
}
for (int i = 0; i < data.LookAtPoint.Count; i++)
{
GameObject LookAtobj = Instantiate(LookAtPoint, data.LookAtPoint[i], Quaternion.identity);
LookAtList.Add(LookAtobj);
BoxCollider boxCollider = LookAtobj.GetComponent<BoxCollider>();
boxCollider.size = data.LookAtSizes[i];
boxCollider.center = data.LookAtCenter[i];
}
//kasuga
for (int i = 0; i < data.DestroyEnemy.Count; i++)
{
GameObject DestroyEnemyobj = Instantiate(DestroyEnemy, data.DestroyEnemy[i], Quaternion.identity);
DestroyEnemyList.Add(DestroyEnemyobj);
BoxCollider boxCollider = DestroyEnemyobj.GetComponent<BoxCollider>();
boxCollider.size = data.DestroyEnemySizes[i];
boxCollider.center = data.DestroyEnemyCenter[i];
}
player = GameObject.FindGameObjectWithTag("Player");
t = player.GetComponent<target>();
R = GameObject.FindGameObjectWithTag("Player").GetComponent<RespawnManager>();
}
// Update is called once per frame
void Update()
{
//デバッグ用にMでチェックポイントを順に移動できる
if (Input.GetKeyDown(KeyCode.M))
{
if (i > data.CheckPoints.Count)
{
i = 0;
LookAtUP();
player.transform.position = data.CheckPoints[i];
}
else
{
i++;
LookAtUP();
player.transform.position = data.CheckPoints[i];
}
}
if (R.lookup == true)
{
for (int i = 0; i < data.CheckPoints.Count; i++)
{
if (R.CPobj.transform.position == data.CheckPoints[i])
{
Debug.Log("lookup" + i);
R.lookpos = data.LookAtPoint[i];
R.lookatobj = LookAtList[i];
R.lookup = false;
}
}
}
//kasuga
for (int i = 0; i < data.CheckPoints.Count; i++)
{
if (R.position == data.CheckPoints[i])
{
areaon = i - 1;
if (areaon >= 0)
{
desenobj = DestroyEnemyList[areaon];
desenobj.gameObject.SetActive(true);
}
}
}
}
void LookAtUP()
{
if (LAP == true)
{
player.transform.LookAt(data.LookAtPoint[i]);
LAP = false;
}
}
#if UNITY_EDITOR
void OnDrawGizmosSelected()
{
//リスポーン地点の順番表示用テキストのサイズと色設定
labelStyle.fontSize = 50;
labelStyle.normal.textColor = Color.white;
//リスポーン地点のGizmo表示
for (int i = 0; i < data.CheckPoints.Count; i++)
{
Gizmos.color = Color.yellow;
Gizmos.DrawSphere(data.CheckPoints[i],0.5f);
Gizmos.color = Color.green;
Gizmos.DrawWireCube(data.CheckPoints[i] + data.colliderCenter[i], data.colliderSizes[i]);
Vector3 worldPos = data.CheckPoints[i] + Vector3.up;
UnityEditor.Handles.Label(worldPos, (i + 1).ToString());
}
for (int i = 0; i < data.LookAtPoint.Count; i++)
{
Gizmos.color = Color.white;
Gizmos.DrawSphere(data.LookAtPoint[i], 0.5f);
Gizmos.color = Color.red;
Gizmos.DrawWireCube(data.LookAtPoint[i] + data.LookAtCenter[i], data.LookAtSizes[i]);
Vector3 worldPos = data.LookAtPoint[i] + Vector3.up;
UnityEditor.Handles.Label(worldPos, (i + 1).ToString());
}
for (int i = 0; i < data.DestroyEnemy.Count; i++)
{
Gizmos.color = Color.cyan;
Gizmos.DrawSphere(data.DestroyEnemy[i], 0.5f);
Gizmos.color = Color.blue;
Gizmos.DrawWireCube(data.DestroyEnemy[i] + data.DestroyEnemyCenter[i], data.DestroyEnemySizes[i]);
Vector3 worldPos = data.DestroyEnemy[i] + Vector3.up;
UnityEditor.Handles.Label(worldPos, (i + 1).ToString());
}
}
#endif
}
コメントを残す