Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ゲーム開発研修(出題と解答編)【MIXI 23新卒技術研修】

ゲーム開発研修(出題と解答編)【MIXI 23新卒技術研修】

23新卒技術研修で実施したゲーム開発研修の講義資料(出題と解答編)です。
後ほど動画も公開します。

ハンズオン用リポジトリ:https://github.com/mixigroup/2023BeginnerTrainingUnityPublic
※この資料の範囲であるコードは公開していないので、ハンズオンを実際に試していただくことはできません。

<< ゲーム開発研修資料一覧 >>
chapter0.ゲームUX編
https://speakerdeck.com/mixi_engineers/2023-game-development-training-chapter0-ux

chapter1.C#講座編
https://speakerdeck.com/mixi_engineers/2023-game-development-training-chapter1-csharp

chapter2.Unity基礎編
https://speakerdeck.com/mixi_engineers/2023-game-development-training-chapter2-unity

chapter3.課題ゲーム紹介編
https://speakerdeck.com/mixi_engineers/2023-game-development-training-chapter3-problem

chapter3.ハンズオン1編
https://speakerdeck.com/mixi_engineers/2023-game-development-training-chapter3-handson1

chapter4.ハンズオン2編
https://speakerdeck.com/mixi_engineers/2023-game-development-training-chapter4-handson2

出題と解答編
https://speakerdeck.com/mixi_engineers/2023-game-development-training-problem-and-answer

資料の利用について
公開している資料は勉強会や企業の研修などで自由にご利用頂いて大丈夫ですが、以下の形での利用だけご遠慮ください。
・受講者から参加費や授業料などを集める形での利用(会場費や飲食費など勉強会運営に必要な実費を集めるのは問題ありません)
・出典を削除または改変しての利用

MIXI ENGINEERS

May 08, 2023
Tweet

More Decks by MIXI ENGINEERS

Other Decks in Technology

Transcript

  1. 出題シートの使い方 • 太字になっている部分がヒント ◦ @todo がついている箇所は自分で考えて実装 ◦ それ以外の箇所は、コメントや前後のコードを見て適切な位置に写す Part1-1 アイテム取得

    Assets/AthreticPrj/Scripts/Gamesystems/Controller/ CharacterItemPickController.cs // 関数に追記 IItemHandler[] ItemTest() { // @todo // physics.spherecastall() など // 検索対象 : TagDefine.item // 返却値 : collider.GetComponent<IItemHandler>() } 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
  2. 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<CharacterController>() // 返却値 : 手錠使用成功 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
  3. // 関数に追記 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<Text>(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
  4. 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<IItemHandler>() 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<IItemHandler>()) .ToArray(); } 1
  5. 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
  6. 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<CharacterController>()) .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
  7. { 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
  8. { 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<Text>(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
  9. 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<CharacterController>() 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);
  10. // @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()を参考に、吹き飛ばしを同期する処理を実装する
  11. 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<Scoreborad.SortPlayer> 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<Image>().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
  12. 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
  13. 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
  14. 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<Image>(); uiText = GetComponentInChildren<Text>(); 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
  15. } 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<HUDPingController>(); pingController.transform.parent = hudroot; // ... 以下略 ... edugames-athletic/Assets/AthreticPrj/Scripts/Gamesystems/System/ GameSystemManager.cs 5
  16. // 関数に以下を追記 public void Action(KeyInputManager i) { // ... 中略

    ... // 最後に追記 // @todo 任意のキーが押されたらピンを立てる // if (Input.GetKeyDown()) など // @todo カメラからレイを飛ばし、当たった箇所にピンを移動 // Camera.ScreenPointToRay() // ピンの位置(world座標) : pingController.targetWorldPos に設定 } 6
  17. 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<GameMonitoringWindow>(); } 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