Slide 1

Slide 1 text

ʲ2೔໨ʳग़୊γʔτ action game development with Unity

Slide 2

Slide 2 text

出題シートの使い方 ● 太字になっている部分がヒント ○ @todo がついている箇所は自分で考えて実装 ○ それ以外の箇所は、コメントや前後のコードを見て適切な位置に写す Part1-1 アイテム取得 Assets/AthreticPrj/Scripts/Gamesystems/Controller/ CharacterItemPickController.cs // 関数に追記 IItemHandler[] ItemTest() { // @todo // physics.spherecastall() など // 検索対象 : TagDefine.item // 返却値 : collider.GetComponent() } Part1-2 回復アイテムの使用 Assets/AthreticPrj/Scripts/Gamesystems/SceneHandler/ GameScene.cs // 関数に追記 public void OnUse() { Debug.Log($"use {selectitem.master.name}"); // @todo 回復アイテムの使用 // 条件 : selectitem.master.type == ItemType.Cure // 回復関数 : mainplayer.Cure() // 回復量 : selectitem.master.value // アイテムの消費 ItemFactory.UseitemInstance(selectitem); } Assets/AthreticPrj/Scripts/Gamesystems/Controller/ /CharacterController.cs // CharacterController classに追記 // デバッグ用のアイテムを作成する public ItemInstanceDetail GetDummyItem(ItemType type, float value = 0) { var dummyItemInstance = new ItemInstance() { id = -1, owner = userid }; var dummyItemMaster = new ItemMaster() { id = -1, type = type, value = value }; return new ItemInstanceDetail() { instace = dummyItemInstance, master = dummyItemMaster }; } [ContextMenu("[Debug] Use Item Cure 20")] public void DebugItemCure() { // @todo 回復アイテムを使うContextMenuを作成 // デバッグ用アイテム : GetDummyItem(ItemType.Cure, 20) // アイテム使用 : 使いたいアイテムを GameScene.Instance.selectitem に設定し // GameScene.OnUse() で使用する } 1

Slide 3

Slide 3 text

Part1-3 手錠アイテムの使用 Assets/AthreticPrj/Scripts/Gamesystems/Controller/ CharacterController.cs // CharacterController classに追加 public GameScene.NicknameContainer nicknameui; // デバッグ用のContextMenu [ContextMenu("(Debug) Freeze Character")] public void DebugFreeze() { Freeze(true); } public bool TryHandcuff() { CharacterController target = null; // @todo 手錠の使用対象を検索 // physics.spherecastcall() など // 検索対象 : TagDefine.character // target : collider.GetComponent() // 返却値 : 手錠使用成功 or 失敗 // sync if (RaiseEventAction.IsConnected) target.rpctarget.RequestFreeze(true); // for single else target.Freeze(true); return true; } // CharacterController classに追加 Coroutine unfreezeCoroutine; public IEnumerator UnfreezeCoroutine(float time) { // @todo 時間経過で拘束解除 // 拘束解除 : Freeze(false) } // 関数に追記 public void Freeze(bool flg) { var s = _status.Freeze(flg); // @todo UnfreezeCoroutineの開始 // sync if (RaiseEventAction.IsConnected) RaiseEventAction.UpdateUserProperty.Status(status); // for single else status = s; } Assets/AthreticPrj/Scripts/Gamesystems/SceneHandler/ GameScene.cs 2

Slide 4

Slide 4 text

// 関数に追記 public void OnUse() { Debug.Log($"use {selectitem.master.name}"); // アイテム消費チェック bool itemUsed = false; // @todo 手錠の使用 // 条件 : selectitem.master.type == ItemType.Handcuff // 手錠使用: mainplayer.TryHandcuff() // アイテム消費チェック if (itemUsed) ItemFactory.UseitemInstance(selectitem); } // 構造体にフィールドを追加 [Serializable] public struct NicknameContainer { public int viewid; public RectTransform ui; // 以下を追記 public Text uitext; public CharacterController character; } // 関数に追記 public void OnCharacterStatusChanged(CharacterController character, PlayerStatus status) { Debug.Log($"OnCharacterStatus Changed: {character.nickname}"); // @todo ニックネーム表示部に拘束状態を表示 // 書き換え対象 : character.nicknameui.uitext.text if (status.freeze) { } else { } } // 関数に追記 void onAddCharacter(CharacterController p) { var r = GameObject.Instantiate(nicknameprefab, nicknameprefab.parent, false); r.gameObject.SetActive(true); var c = new NicknameContainer() { viewid = p.viewid, ui = r, character = p }; // 以下を追記 c.uitext = r.GetComponentInChildren(true); c.uitext.text = p.nickname.ToString(); p.nicknameui = c; if (CameraController.Instance) c = c.SetOffset(CameraController.Instance.setting.nicknameoffset); nicknameobjects.Add(c); p.onStatusChanged += OnCharacterStatusChanged; } 3

Slide 5

Slide 5 text

ʲ2೔໨ʳղ౴ྫγʔτ action game development with Unity

Slide 6

Slide 6 text

Part1-1 アイテム取得 回答例 ● System.Linqはパフォーマンス注意だがコレクション操作が簡単。 ○ Where:条件絞り込み ○ Select:射影 ○ ToArray、ToList:配列/リストに変換 ● Debug.DrawRay()やGizmos.DrawSphere()を利用してレイキャストの表示・デバッグできる Assets/AthreticPrj/Scripts/Gamesystems/Controller/ CharacterItemPickController.cs // 関数に追記 IItemHandler[] ItemTest() { // @todo // physics.spherecastcall() など // 検索対象 : TagDefine.item // 返却値 : collider.GetComponent() var ray = new Ray() { origin = character.mrigidbody.position + character.mcollider.center * character.transform.localScale.y + character.transform.rotation * Vector3.forward, direction = Vector3.down }; var radius = 2f; var range = character.mcollider.center.y * character.transform.localScale.y * 1.75f; Debug.DrawRay(ray.origin, ray.direction * range, Color.red, 0.1f); var hits = Physics.SphereCastAll(ray, radius, range, LayerDefine.Layermask_Invisible); return hits .XWhere(c => c.collider.tag == TagDefine.item) .Select(c => c.collider.GetComponent()) .ToArray(); } 1

Slide 7

Slide 7 text

Part1-2 回復アイテムの使用 回答例 ● 後のために、ItemTypeで各アイテム分の分岐を作っておいた Assets/AthreticPrj/Scripts/Gamesystems/SceneHandler/ GameScene.cs // 関数に追記 public void OnUse() { Debug.Log($"use {selectitem.master.name}"); // [Q 回復アイテム] // TODO: ItemTypeで分岐して、mainplayer.Cure()を呼び出す switch (selectitem.master.type) { case ItemType.Handcuff: break; case ItemType.Cure: mainplayer.Cure((byte)selectitem.master.value); break; case ItemType.Gamepoint: break; case ItemType.Device: //仕掛け爆弾 break; } ItemFactory.UseitemInstance(selectitem); } ● 解答例ではデバッグ用に、回復アイテムを使うContextMenuを用意した Assets/AthreticPrj/Scripts/Gamesystems/Controller/ CharacterController.cs // CharacterController classに追記 public ItemInstanceDetail GetDummyItem(ItemType type, float value = 0) { var dummyItemInstance = new ItemInstance() { id = -1, owner = userid }; var dummyItemMaster = new ItemMaster() { id = -1, type = type, value = value }; return new ItemInstanceDetail() { instace = dummyItemInstance, master = dummyItemMaster }; } [ContextMenu("[Debug] Use Item Cure 20")] public void DebugItemCure() { GameScene.Instance.selectitem = GetDummyItem(ItemType.Cure, 20); GameScene.Instance.OnUse(); } 1

Slide 8

Slide 8 text

Part1-3 手錠アイテムの使用 回答例 ● 有効範囲内にキャラがいなければreturn false ● レイキャスト結果を自分と近い順に並べ、一番近くのキャラを拘束すると自然に見える Assets/AthreticPrj/Scripts/Gamesystems/Controller/ CharacterController.cs // CharacterController classに追加 public GameScene.NicknameContainer nicknameui; // デバッグ用のContextMenu [ContextMenu("(Debug) Freeze Character")] public void DebugFreeze() { Freeze(true); } public bool TryHandcuff() { // @todo 手錠の使用対象を検索 CharacterController target = null; var ray = new Ray() { origin = mrigidbody.position + mcollider.center * transform.localScale.y + transform.rotation * Vector3.forward, direction = Vector3.down }; var radius = 2f; var range = mcollider.center.y * transform.localScale.y * 1.75f; var hits = Physics.SphereCastAll(ray, radius, range, LayerDefine.Layermask_Invisible); target = hits .XWhere(c => c.collider.tag == TagDefine.character) .Select(c => c.collider.GetComponent()) .OrderBy(c => (c.mrigidbody.position - mrigidbody.position).sqrMagnitude) // 距離順にして最も近いキャラクターを取得 .FirstOrDefault(c => c.userid != userid); if (target == null) return false; // sync if (RaiseEventAction.IsConnected) target.rpctarget.RequestFreeze(true); // for single else target.Freeze(true); return true; } // CharacterController classに追加 Coroutine unfreezeCoroutine; public IEnumerator UnfreezeCoroutine(float time) { // @todo 時間経過で拘束解除 // 拘束解除 : Freeze(false) yield return new WaitForSeconds(time); Freeze(false); } // 関数に追記 public void Freeze(bool flg) 1

Slide 9

Slide 9 text

{ var s = _status.Freeze(flg); // @todo UnfreezeCoroutineの開始 if (flg) unfreezeCoroutine = StartCoroutine(UnfreezeCoroutine(5)); // sync if (RaiseEventAction.IsConnected) RaiseEventAction.UpdateUserProperty.Status(status); // for single else status = s; } ● 範囲内にキャラクターがいるときだけ消費すると納得感がある。 ● 他のプレイヤーからも状態が見えるように、ニックネームに「拘束中」と表示 Assets/AthreticPrj/Scripts/Gamesystems/SceneHandler/ GameScene.cs // 関数に追記 public void OnUse() { Debug.Log($"use {selectitem.master.name}"); // アイテム消費チェック bool itemUsed = false; // @todo 手錠の使用 switch (selectitem.master.type) { case ItemType.Handcuff: itemUsed = mainplayer.TryHandcuff(); break; case ItemType.Cure: mainplayer.Cure((byte)selectitem.master.value); itemUsed = true; break; case ItemType.Gamepoint: break; case ItemType.Device: //仕掛け爆弾 break; } // アイテム消費チェック if (itemUsed) ItemFactory.UseitemInstance(selectitem); } // 構造体にフィールドを追加 [Serializable] public struct NicknameContainer { public int viewid; public RectTransform ui; // 以下を追記 public Text uitext; public CharacterController character; } // 関数に追記 public void OnCharacterStatusChanged(CharacterController character, PlayerStatus status) 2

Slide 10

Slide 10 text

{ Debug.Log($"OnCharacterStatus Changed: {character.nickname}"); // @todo ニックネーム表示部に拘束状態を表示 if (status.freeze) { var uitext = character.nicknameui.uitext; uitext.text = $"{character.nickname}[拘束中]"; uitext.color = Color.red; } else { var uitext = character.nicknameui.uitext; uitext.text = $"{character.nickname}"; uitext.color = Color.white; } } // 関数に追記 void onAddCharacter(CharacterController p) { var r = GameObject.Instantiate(nicknameprefab, nicknameprefab.parent, false); r.gameObject.SetActive(true); var c = new NicknameContainer() { viewid = p.viewid, ui = r, character = p }; // 以下を追記 c.uitext = r.GetComponentInChildren(true); c.uitext.text = p.nickname.ToString(); p.nicknameui = c; if (CameraController.Instance) c = c.SetOffset(CameraController.Instance.setting.nicknameoffset); nicknameobjects.Add(c); p.onStatusChanged += OnCharacterStatusChanged; } 3

Slide 11

Slide 11 text

ʲ2೔໨ʳग़୊γʔτ action game development with Unity

Slide 12

Slide 12 text

Part2-1 パンチ攻撃 Assets/AthreticPrj/Scripts/Gamesystems/Controller/ /CharacterController.cs // 関数に追記 IEnumerator DoAttack() { isattack = true; manimator.SetTrigger(AnimationDefine.CharacterDemo.Attack); mrigidbody.AddRelativeForce(Vector3.forward*setting.pushpower,ForceMode.Impulse); var check = new RepeatChecker(setting.attacktime); // @todo 攻撃対象を取得してダメージを与える。 // physics.spherecastcall() を使用することを想定しています。 // ダメージを与えるための関数はAddDamage()が既に実装されています。 // それを同期するためのRequestAddDamage()も実装済みです。 // 検索対象 : TagDefine.character // target : collider.GetComponent() while (!check.Check) { yield return new WaitForSeconds(setting.attacktime); } isattack = false; yield break; } Part2-2 HPバーの実装 Assets\AthreticPrj\Scripts\Gamesystems\SceneHandler\GameScene.cs void OnCharacterStatusChanged(CharacterController character, PlayerStatus status) { Debug.Log($"OnCharacterStatus Changed: {character.nickname}"); // @todo:uiinstanceから体力ゲージのSliderを取得し、HP割合をセット } Part2-3 パンチ攻撃に吹き飛ばし効果を追加 Assets/AthreticPrj/Scripts/Gamesystems/Controller/ /CharacterController.cs IEnumerator DoAttack() { isattack = true; manimator.SetTrigger(AnimationDefine.CharacterDemo.Attack); mrigidbody.AddRelativeForce(Vector3.forward*setting.pushpower,ForceMode.Impulse); var check = new RepeatChecker(setting.attacktime);

Slide 13

Slide 13 text

// @todo パンチがヒットしたキャラをパンチ方向に吹き飛ばす。 // Rigidbody.AddForce()を使用 while (!check.Check) { yield return new WaitForSeconds(setting.attacktime); } isattack = false; yield break; } Assets/AthreticPrj/Scripts/Gamesystems/IData/PunCharacterHandler.cs // @todo RequestAddDamage()を参考に、吹き飛ばしを同期する処理を実装する

Slide 14

Slide 14 text

ʲ2೔໨ʳղ౴ྫγʔτ action game development with Unity

Slide 15

Slide 15 text

εϥΠυ/ਖ਼ղϦϙδτϦࢀর

Slide 16

Slide 16 text

ʻMIXI 23ଔήʔϜݚमʼ Game Develop ࣮ફ՝୊ ࣗ༝બ୒՝୊ extra missionग़୊γʔτ

Slide 17

Slide 17 text

1.仲間の方を向く edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/Controller/ CharacterController.cs // CharacterController classに追加 public void LookAtTargetPlayer(CharacterController character) { Debug.Log($"player lookat ... {character.nickname}"); // @todo 指定された仲間(character)の方を向く // Quaternion.LookRotation など // 条件 : character != this if (character != this) { var rot = Quaternion.LookRotation(character.transform.position - transform.position); transform.rotation = rot; } } edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/SceneHandler/ MemberlistScene.cs // 関数を編集 public void AttachList(List memberlist) { AttachListCommand( memberlist .OrderByDescending(c => c.point) //.OrderBy(c => c.team) // 以下を編集、ボタンのコールバックを設定 .Select(c => new MenuCommand { text = c.label, value = c, method = () => OnClickMemberButton(c) }) .ToList(), (m,g)=> { SceneUtility.defaultAttachMenuCommandOnCreate(m, g); g.GetComponent().color = ConnectScene.teambuttonsettings[(int) ((Scoreborad.SortPlayer)m.value).team ].color; } ); } // MemberListScene classに追加 CharacterController lastChoice; private void OnClickMemberButton(Scoreborad.SortPlayer player){ Debug.Log($"OnClickMemberButton :{player.label}"); // check if ingame if (GameSystemManager.Instance && GameSystemManager.Instance.IsPlaying) { var character = GameSystemManager.GetCharacters().Find(r => r.userid == player.userid); if (character == lastChoice) character = GameSystemManager.Instance.mainplayer; lastChoice = character; // @todo LookAtTargetPlayer()呼び出し // 自分のキャラクター : GameSystemManager.Instance.mainplayer // 引数 : character } } 1

Slide 18

Slide 18 text

2.メンバーボタンで3D矢印を表示 edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/Controller/ DirectionArrowController.cs // classに以下を追記 using System; using System.Linq; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Mixigameslib.Networkgame { public class DirectionArrowController : MonoBehaviour { static DirectionArrowController instance; public static DirectionArrowController Instance => instance; private CharacterController _targetchara; public CharacterController targetchara { get => _targetchara; set { _targetchara = value; var isActive = GameSystemManager.Ismine(_targetchara) == false; gameObject.SetActive(isActive); } } private void Awake() { instance = this; } private void OnDestroy() { instance = null; } private void LateUpdate() { UpdateLookAtArrow(); } public void UpdateLookAtArrow() { // @todo // transformの向きと位置をSetPositionAndRotation()で設定する // 向く対象 : targetchara // 自分のキャラクター : GameScene.mainplayer } } } edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/SceneHandler/ MemberlistScene.cs // 関数に追記 private void OnClickMemberButton(Scoreborad.SortPlayer player){ Debug.Log($"OnClickMemberButton :{player.label}"); // check if ingame 2

Slide 19

Slide 19 text

if (GameSystemManager.Instance && GameSystemManager.Instance.IsPlaying) { var character = GameSystemManager.GetCharacters().Find(r => r.userid == player.userid); if (character == lastChoice) character = GameSystemManager.Instance.mainplayer; lastChoice = character; // @todo // ボタン押下でDirectionArrowController.Instance.targetを設定する // 条件 : DirectionArrowController.Instance != null のとき // 設定する値 : character } } edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/System/ GameSettings.cs // ResourceDefineに以下を追記 public static class ResourceDefine { public static string[] prefabpath = new string[] { "System/GameCamera", //0 // ... 中略 ... // 以下を追加、prefabのファイルパスを設定してロードできるように "System/Directionarrow3d", //10 }; public static string[] prefabpath_remote = new string[] { "System/GameCamera", //0 // ... 中略 ... // 以下を追加 "System/Directionarrow3d", //10 }; edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/System/ GameSystemManager.cs // GameSystemManager classに以下を追加 // prefabファイルパスのindex public int loadarrowindex = 10; public GameObject directionArrow; // 関数に以下を追記 IEnumerator Start() { // ... 中略 ... InitCharacter(); // 以下を追記、prefabのロードとInstantiate directionArrow = ResourceManager.Instantiate(loadarrowindex); LoadCamera(); // ... 以下略 ... 3

Slide 20

Slide 20 text

3. ピン表示 edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/Controller/ HUDPingController.cs using System; using System.Linq; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace Mixigameslib.Networkgame { public class HUDPingController : MonoBehaviour { // 表示中のpingをキャンセルする距離 public float pingCancelSqrDistance = 2500; public Image uiImage; public Text uiText; private Vector3 _targetWorldPos; public Vector3 targetWorldPos { get => _targetWorldPos; set { _targetWorldPos = value; uiText.text = GameScene.mainplayer.nickname; // @todo 今のピン位置から近い場所を指定した場合、ピンをOn/Offする // 条件 : (transform.position - pingScreenPos).sqrMagnitude < [しきい値] // On/Off状態 : gameObject.activeInHierarchy // On/Off切替 : gameObject.SetActive() var pingScreenPos = GetPingScreenPos(); } } void Awake() { uiImage = GetComponentInChildren(); uiText = GetComponentInChildren(); gameObject.SetActive(false); } void LateUpdate() { UpdatePingPosition(); } Vector3 GetPingScreenPos() { // @todo ワールド座標からスクリーン座標に変換 // Camera.WorldToScreenPoint(ワールド座標) // カメラ : CameraController.Instance.worldcamera // 条件 : スクリーン座標.z <=0 のとき、画面外 return default; } void UpdatePingPosition() { // @todo ピンの位置を設定(スクリーン座標) // 条件 : gameObject.activeInHierarchy == true } } 4

Slide 21

Slide 21 text

} edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/System/ GameSettings.cs // ResourceDefineに以下を追記 // ResourceDefineに以下を追記 public static class ResourceDefine { public static string[] prefabpath = new string[] { "System/GameCamera", //0 // ... 中略 ... // 10番は他の課題で使用 "", //10 // 以下を追記、アセットロードのためにパスを追加 "UI/Prefab/Parts/playerping", //11 }; public static string[] prefabpath_remote = new string[] { "System/GameCamera", //0 // ... 中略 ... // 10番は他の課題で使用 "", //10 // 以下を追記、アセットロードのためにパスを追加 "UI/Prefab/Parts/playerping", //11 }; edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/Controller/ GameScene.cs // GameScene classに以下を追加 public int loadpingindex = 11; public HUDPingController pingController; // 関数に以下を追記 protected override void Awake() { // ... 中略 ... radar = Instantiate(radarprefab,hudroot,false).transform as RectTransform; // pingControllerの生成 pingController = ResourceManager.Instantiate(loadpingindex).GetComponent(); pingController.transform.parent = hudroot; // ... 以下略 ... edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/System/ GameSystemManager.cs 5

Slide 22

Slide 22 text

// 関数に以下を追記 public void Action(KeyInputManager i) { // ... 中略 ... // 最後に追記 // @todo 任意のキーが押されたらピンを立てる // if (Input.GetKeyDown()) など // @todo カメラからレイを飛ばし、当たった箇所にピンを移動 // Camera.ScreenPointToRay() // ピンの位置(world座標) : pingController.targetWorldPos に設定 } 6

Slide 23

Slide 23 text

4. 瞬間移動の検知と表示 edugames-athletic/Assets/AthreticPrj/Editor/ GameMonitoringWindow.cs // edugames-athletic/Assets/AthreticPrj/Editorフォルダを作成 // Editor配下に以下の内容のC#ファイルを作成 using System.Linq; using System.Collections.Generic; using UnityEngine; using UnityEditor; namespace Mixigameslib.Networkgame { public class GameMonitoringWindow : EditorWindow { static readonly float detectTeleportSqrDistance = 50f; private string labeltext; private Vector3[] positionCache; [MenuItem("custom/GameMonitoringWindow")] public static void Show() { // @todo ウィンドウを作成 // EditorWindow.GetWindow EditorWindow.GetWindow(); } void OnGUI() { // @todo ラベルを作成 // 表示テキスト : labeltext // GUILayout.Label } void Update() { Debug.Log($"editor window update {Application.isPlaying} "); if (Application.isPlaying == false) return; var characters = GameSystemManager.GetCharacters(); // キャラクターが存在しない場合はreturn if (characters == null) return; // キャッシュがない or キャラクターの数が増減した場合 if (positionCache == null || positionCache.Length != characters.Count) { // @todo positionCacheを各キャラクターの現在座標で初期化 } // @todo // 各キャラクターについて、前フレームの位置(positionCache)からの移動距離を計算 // 閾値を超えていたらラベルに表示 // 閾値 : detectTeleportSqrDistance // ラベル更新 : labeltextを変更したあと、Repaint();を呼び出す } } } 7

Slide 24

Slide 24 text

ʲ2೔໨ʳGame Develop ࣮ફ՝୊ ࣗ༝બ୒ ՝୊ extra missionղ౴ྫ

Slide 25

Slide 25 text

εϥΠυ/ਖ਼ղϦϙδτϦࢀর