Last active
November 12, 2024 05:44
-
-
Save Curookie/5de19e581eb54cff7d7b643408ba930c to your computer and use it in GitHub Desktop.
Unity 유니티 실무
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
글 로딩 이슈로 앞으로 유니티 실무2에 적겠습니다. | |
실무2 - https://gist.github.com/Curookie/42c979a7de7d656ec8cf6b8c01ea0457 | |
----- 절차적 콘텐츠 생성(Procedural Content Generation, PCG) ----- | |
랜덤 생성 알고리즘 | |
1. Cellular Automaton | |
- 2D Platformer 맵 생성시 사용할 수 있음. (동굴, 던전) | |
2. Dungeon Room 생성 알고리즘 (BSP) | |
Binary Space Partitioning (BSP 알고리즘) | |
- 던전과 같은 방을 구분하고 연결하는 데 사용 | |
- 맵 생성의 가장 기본적인 알고리즘 | |
using UnityEngine; | |
using System.Collections.Generic; | |
public class BSPNode | |
{ | |
public RectInt rect; // 노드가 차지하는 전체 영역 | |
public RectInt room; // 노드 내의 방 영역 | |
public BSPNode left; // 좌측 자식 노드 | |
public BSPNode right; // 우측 자식 노드 | |
public int minRoomSize = 5; // 방의 최소 크기 | |
public int maxRoomSize = 12; // 방의 최대 크기 | |
public BSPNode(RectInt rect) | |
{ | |
this.rect = rect; | |
} | |
// 현재 노드를 자식 노드로 분할 | |
public bool Split() | |
{ | |
// 가로 또는 세로로 분할을 결정 | |
bool splitHorizontally = Random.Range(0, 2) == 0; | |
// 분할 가능 여부 확인 | |
if (rect.width < minRoomSize * 2 && rect.height < minRoomSize * 2) | |
{ | |
return false; // 분할 불가능 | |
} | |
// 분할 방향에 따라 적절한 최대 길이를 계산 | |
if (splitHorizontally) | |
{ | |
if (rect.height < minRoomSize * 2) return false; | |
int splitY = Random.Range(minRoomSize, rect.height - minRoomSize); | |
left = new BSPNode(new RectInt(rect.x, rect.y, rect.width, splitY)); | |
right = new BSPNode(new RectInt(rect.x, rect.y + splitY, rect.width, rect.height - splitY)); | |
} | |
else | |
{ | |
if (rect.width < minRoomSize * 2) return false; | |
int splitX = Random.Range(minRoomSize, rect.width - minRoomSize); | |
left = new BSPNode(new RectInt(rect.x, rect.y, splitX, rect.height)); | |
right = new BSPNode(new RectInt(rect.x + splitX, rect.y, rect.width - splitX, rect.height)); | |
} | |
return true; | |
} | |
// 방을 생성하는 함수 (최소 및 최대 방 크기 내에서 랜덤한 크기의 방을 생성) | |
public void CreateRoom() | |
{ | |
int roomWidth = Random.Range(minRoomSize, Mathf.Min(maxRoomSize, rect.width)); | |
int roomHeight = Random.Range(minRoomSize, Mathf.Min(maxRoomSize, rect.height)); | |
int roomX = Random.Range(rect.x, rect.x + rect.width - roomWidth); | |
int roomY = Random.Range(rect.y, rect.y + rect.height - roomHeight); | |
room = new RectInt(roomX, roomY, roomWidth, roomHeight); | |
} | |
// 좌우 자식 노드에 방이 있을 경우, 두 방을 연결하는 통로를 생성 | |
public List<RectInt> ConnectRooms() | |
{ | |
if (left == null || right == null) | |
{ | |
return new List<RectInt>(); | |
} | |
// 왼쪽 방과 오른쪽 방의 중심을 기준으로 통로 연결 | |
Vector2Int leftCenter = new Vector2Int(left.room.x + left.room.width / 2, left.room.y + left.room.height / 2); | |
Vector2Int rightCenter = new Vector2Int(right.room.x + right.room.width / 2, right.room.y + right.room.height / 2); | |
List<RectInt> corridors = new List<RectInt>(); | |
// 가로 통로 생성 | |
if (leftCenter.x != rightCenter.x) | |
{ | |
RectInt corridor = new RectInt(Mathf.Min(leftCenter.x, rightCenter.x), leftCenter.y, Mathf.Abs(leftCenter.x - rightCenter.x), 1); | |
corridors.Add(corridor); | |
} | |
// 세로 통로 생성 | |
if (leftCenter.y != rightCenter.y) | |
{ | |
RectInt corridor = new RectInt(rightCenter.x, Mathf.Min(leftCenter.y, rightCenter.y), 1, Mathf.Abs(leftCenter.y - rightCenter.y)); | |
corridors.Add(corridor); | |
} | |
return corridors; | |
} | |
} | |
using UnityEngine; | |
using System.Collections.Generic; | |
public class BSPDungeonGenerator : MonoBehaviour | |
{ | |
public int width = 50; // 던전의 너비 | |
public int height = 50; // 던전의 높이 | |
public int maxDepth = 5; // 트리의 최대 깊이 | |
public GameObject roomPrefab; // 방을 나타내는 프리팹 | |
private BSPNode rootNode; | |
private List<RectInt> rooms; | |
private List<RectInt> corridors; | |
void Start() | |
{ | |
GenerateDungeon(); | |
} | |
void GenerateDungeon() | |
{ | |
rootNode = new BSPNode(new RectInt(0, 0, width, height)); | |
SplitTree(rootNode, 0); | |
rooms = new List<RectInt>(); | |
corridors = new List<RectInt>(); | |
CollectRoomsAndCorridors(rootNode); | |
DrawDungeon(); | |
} | |
// 트리를 재귀적으로 분할 | |
void SplitTree(BSPNode node, int depth) | |
{ | |
if (depth >= maxDepth || !node.Split()) | |
{ | |
node.CreateRoom(); // 분할할 수 없으면 방을 생성 | |
return; | |
} | |
SplitTree(node.left, depth + 1); | |
SplitTree(node.right, depth + 1); | |
} | |
// 트리에서 방과 통로를 수집 | |
void CollectRoomsAndCorridors(BSPNode node) | |
{ | |
if (node.left == null && node.right == null) | |
{ | |
rooms.Add(node.room); // 잎 노드(leaf)에 있는 방 수집 | |
} | |
else | |
{ | |
if (node.left != null) CollectRoomsAndCorridors(node.left); | |
if (node.right != null) CollectRoomsAndCorridors(node.right); | |
// 좌우 자식 노드를 연결하는 통로 수집 | |
corridors.AddRange(node.ConnectRooms()); | |
} | |
} | |
// 방과 통로를 시각적으로 표시 | |
void DrawDungeon() | |
{ | |
foreach (var room in rooms) | |
{ | |
Vector3 position = new Vector3(room.x + room.width / 2, room.y + room.height / 2, 0); | |
GameObject roomInstance = Instantiate(roomPrefab, position, Quaternion.identity); | |
roomInstance.transform.localScale = new Vector3(room.width, room.height, 1); | |
} | |
foreach (var corridor in corridors) | |
{ | |
Vector3 position = new Vector3(corridor.x + corridor.width / 2, corridor.y + corridor.height / 2, 0); | |
GameObject corridorInstance = Instantiate(roomPrefab, position, Quaternion.identity); | |
corridorInstance.transform.localScale = new Vector3(corridor.width, corridor.height, 1); | |
} | |
} | |
} | |
using System.Collections.Generic; | |
using UnityEngine; | |
public class SimpleBSPDepth : MonoBehaviour | |
{ | |
public int maxDepth = 3; // BSP 분할 최대 깊이 | |
public GameObject roomPrefab; // 방을 나타내는 프리팹 (크기 없음, 단순 시각화용) | |
private List<BSPNode> rooms; // 최종적으로 생성된 방 리스트 | |
void Start() | |
{ | |
// 루트 노드 생성 (크기 없이 분할 시작) | |
BSPNode rootNode = new BSPNode(); | |
rooms = new List<BSPNode>(); | |
// BSP 분할 시작 | |
SplitNode(rootNode, 0); | |
// 던전을 시각화 | |
DrawDungeon(); | |
} | |
// 노드를 분할하는 함수 (BSP의 분할 깊이에 따라 처리) | |
void SplitNode(BSPNode node, int depth) | |
{ | |
// 최대 분할 깊이에 도달했으면 방을 추가 | |
if (depth >= maxDepth) | |
{ | |
rooms.Add(node); // 분할하지 않고 방을 최종 노드로 추가 | |
return; | |
} | |
// 노드 분할 (좌우 또는 상하로 랜덤하게 분할) | |
node.Split(); | |
// 자식 노드를 재귀적으로 분할 | |
SplitNode(node.left, depth + 1); | |
SplitNode(node.right, depth + 1); | |
} | |
// 방을 시각화하는 함수 | |
void DrawDungeon() | |
{ | |
int offset = 5; // 방 간의 거리 오프셋 | |
foreach (BSPNode node in rooms) | |
{ | |
// 랜덤한 상하 또는 좌우 배치를 위한 좌표 설정 | |
Vector3 position = new Vector3(node.position.x * offset, node.position.y * offset, 0); | |
Instantiate(roomPrefab, position, Quaternion.identity); // 방을 임의 위치에 배치 | |
} | |
} | |
} | |
// BSP 노드 클래스 (크기 없이 분할만 처리) | |
public class BSPNode | |
{ | |
public BSPNode left; // 좌측 자식 노드 | |
public BSPNode right; // 우측 자식 노드 | |
public Vector2Int position; // 각 노드의 위치 (분할 시 이동 방향에 따른 좌표) | |
// 생성자 | |
public BSPNode() | |
{ | |
position = Vector2Int.zero; // 초기 위치를 (0, 0)으로 설정 | |
} | |
// 노드를 분할하는 함수 (크기 고려 없이 단순히 상하 또는 좌우로 분할) | |
public void Split() | |
{ | |
// 분할 방향 결정 (0: 좌우, 1: 상하) | |
bool splitHorizontally = Random.Range(0, 2) == 0; | |
// 좌우로 분할 | |
if (splitHorizontally) | |
{ | |
left = new BSPNode { position = new Vector2Int(position.x - 1, position.y) }; // 왼쪽으로 이동 | |
right = new BSPNode { position = new Vector2Int(position.x + 1, position.y) }; // 오른쪽으로 이동 | |
} | |
// 상하로 분할 | |
else | |
{ | |
left = new BSPNode { position = new Vector2Int(position.x, position.y + 1) }; // 위쪽으로 이동 | |
right = new BSPNode { position = new Vector2Int(position.x, position.y - 1) }; // 아래쪽으로 이동 | |
} | |
} | |
} | |
----- 밀도 기반 경로 탐색 알고리즘 (AI 관련) : 군중 제어, 군집 이동에 관한 알고리즘 ----- | |
충돌이나 혼잡을 피하기 위해 경로를 동적으로 조종하는 알고리즘 | |
using UnityEngine; | |
using UnityEngine.AI; | |
public class AgentDensityController : MonoBehaviour | |
{ | |
private NavMeshAgent agent; // NavMeshAgent 컴포넌트 | |
public Transform target; // 목표 지점 (Destination) | |
public float detectionRadius = 5.0f; // 밀도를 계산할 반경 | |
public LayerMask agentLayer; // 에이전트가 속한 레이어 (밀도 계산용) | |
private Vector3 originalDestination; // 에이전트의 최종 목표 지점 | |
private float densityCheckInterval = 0.5f; // 밀도 계산 주기 | |
private float timeSinceLastCheck = 0.0f; // 밀도 계산을 위한 시간 누적 값 | |
// 초기화 | |
void Start() | |
{ | |
agent = GetComponent<NavMeshAgent>(); // NavMeshAgent 컴포넌트 가져오기 | |
originalDestination = target.position; // 목표 지점 설정 | |
agent.SetDestination(originalDestination); // 목표로 이동 시작 | |
} | |
// 매 프레임마다 밀도 계산 및 경로 조정 | |
void Update() | |
{ | |
timeSinceLastCheck += Time.deltaTime; | |
if (timeSinceLastCheck >= densityCheckInterval) | |
{ | |
float density = CalculateDensity(); // 밀도 계산 | |
AdjustMovementBasedOnDensity(density); // 밀도에 따라 경로 조정 | |
timeSinceLastCheck = 0.0f; // 밀도 체크 시간 초기화 | |
} | |
} | |
// 주변 에이전트 밀도를 계산하는 함수 | |
float CalculateDensity() | |
{ | |
Collider[] agentsNearby = Physics.OverlapSphere(transform.position, detectionRadius, agentLayer); | |
return agentsNearby.Length; // 주변 에이전트 수를 밀도로 간주 | |
} | |
// 밀도에 따라 경로를 재조정하는 함수 | |
void AdjustMovementBasedOnDensity(float density) | |
{ | |
if (density > 5) // 밀도가 높으면 경로 회피 | |
{ | |
Vector3 avoidanceDirection = GetAvoidanceDirection(); // 새로운 회피 경로 계산 | |
agent.SetDestination(avoidanceDirection); // 회피 경로로 이동 | |
} | |
else // 밀도가 낮으면 원래 목표 지점으로 이동 | |
{ | |
agent.SetDestination(originalDestination); | |
} | |
} | |
// 밀도가 높은 구역을 피할 회피 경로를 계산하는 함수 | |
Vector3 GetAvoidanceDirection() | |
{ | |
Vector3 randomDirection = Random.insideUnitSphere * 10.0f; // 무작위 방향으로 회피 | |
randomDirection += transform.position; // 현재 위치에서 회피 방향을 계산 | |
NavMeshHit hit; // NavMesh 내에서 유효한 위치 탐색 | |
NavMesh.SamplePosition(randomDirection, out hit, 10.0f, NavMesh.AllAreas); | |
return hit.position; // 유효한 회피 경로 반환 | |
} | |
} | |
// 다수의 에이전트를 생성하여 밀도 기반 경로 탐색을 테스트하는 스크립트 | |
public class AgentSpawner : MonoBehaviour | |
{ | |
public GameObject agentPrefab; // 에이전트 프리팹 | |
public int agentCount = 50; // 생성할 에이전트 수 | |
// Start에서 에이전트 생성 | |
void Start() | |
{ | |
for (int i = 0; i < agentCount; i++) | |
{ | |
// 임의 위치에 에이전트를 생성 | |
Vector3 spawnPosition = new Vector3(Random.Range(-10, 10), 0, Random.Range(-10, 10)); | |
Instantiate(agentPrefab, spawnPosition, Quaternion.identity); | |
} | |
} | |
} | |
----- Boid 알고리즘 (AI 관련) : 군중 제어, 군집 이동에 관한 알고리즘 ----- | |
새, 물고기, 군중 충돌하지 않고 자연스럽게 군집해서 이동할때 사용하는 알고리즘 | |
가중치와 경계처리 포함해서 각각 (정렬, 응집, 분리) 방향 벡터를 구해서 이동 부분에 넣어주면 됨. | |
using UnityEngine; | |
using System.Collections.Generic; | |
public class Boid : MonoBehaviour | |
{ | |
public float speed = 5.0f; | |
public float neighborRadius = 3.0f; | |
public float separationDistance = 1.0f; | |
public Vector3 bounds = new Vector3(50, 50, 50); // 시뮬레이션 영역 | |
private Vector3 velocity; | |
// 각 에이전트의 행동을 계산하는 함수 | |
void Update() | |
{ | |
List<Boid> neighbors = GetNeighbors(); | |
Vector3 alignment = Align(neighbors) * alignmentWeight; | |
Vector3 cohesion = Cohere(neighbors) * cohesionWeight; | |
Vector3 separation = Separate(neighbors) * separationWeight; | |
Vector3 boundaryForce = CheckBounds(); | |
Vector3 moveDirection = alignment + cohesion + separation + boundaryForce; | |
velocity = Vector3.Lerp(velocity, moveDirection, Time.deltaTime); | |
transform.position += velocity * Time.deltaTime * speed; | |
transform.forward = velocity.normalized; | |
} | |
// 근처의 보이드 리스트를 가져오는 함수 | |
List<Boid> GetNeighbors() | |
{ | |
Collider[] colliders = Physics.OverlapSphere(transform.position, neighborRadius); | |
List<Boid> neighbors = new List<Boid>(); | |
foreach (Collider collider in colliders) | |
{ | |
Boid boid = collider.GetComponent<Boid>(); | |
if (boid != null && boid != this) | |
{ | |
neighbors.Add(boid); | |
} | |
} | |
return neighbors; | |
} | |
// 정렬(Alignment) 규칙 | |
Vector3 Align(List<Boid> neighbors) | |
{ | |
Vector3 avgDirection = Vector3.zero; | |
if (neighbors.Count == 0) return avgDirection; | |
foreach (Boid neighbor in neighbors) | |
{ | |
avgDirection += neighbor.velocity; | |
} | |
avgDirection /= neighbors.Count; | |
return avgDirection.normalized; // 평균 방향으로 정렬 | |
} | |
// 응집(Cohesion) 규칙 | |
Vector3 Cohere(List<Boid> neighbors) | |
{ | |
Vector3 centerOfMass = Vector3.zero; | |
if (neighbors.Count == 0) return centerOfMass; | |
foreach (Boid neighbor in neighbors) | |
{ | |
centerOfMass += neighbor.transform.position; | |
} | |
centerOfMass /= neighbors.Count; | |
return (centerOfMass - transform.position).normalized; // 중심으로 이동 | |
} | |
// 분리(Separation) 규칙 | |
Vector3 Separate(List<Boid> neighbors) | |
{ | |
Vector3 separationForce = Vector3.zero; | |
foreach (Boid neighbor in neighbors) | |
{ | |
float distance = Vector3.Distance(transform.position, neighbor.transform.position); | |
if (distance < separationDistance) | |
{ | |
separationForce += (transform.position - neighbor.transform.position) / distance; | |
} | |
} | |
return separationForce.normalized; // 가까운 보이드로부터 멀어지기 | |
} | |
// 시뮬레이션 경계를 벗어날 때 중심으로 이동시키는 힘 | |
Vector3 CheckBounds() | |
{ | |
Vector3 boundaryForce = Vector3.zero; | |
if (transform.position.x > bounds.x || transform.position.x < -bounds.x) | |
boundaryForce.x = -transform.position.x; | |
if (transform.position.y > bounds.y || transform.position.y < -bounds.y) | |
boundaryForce.y = -transform.position.y; | |
if (transform.position.z > bounds.z || transform.position.z < -bounds.z) | |
boundaryForce.z = -transform.position.z; | |
return boundaryForce.normalized; | |
} | |
} | |
----- float Mathf.PerlinNoise(float x, float y); ------ | |
절차적 맵 생성, 카메라 쉐이킹 (Camera Shake)에 사용할 수 있는 기본적인 랜덤 함수. | |
결과값은 0에서 1 사이의 값이다. | |
핵심은 0- 1 값이 2차원적으로 랜덤하게 이어져서 나온다는 점. 응용은 자율. | |
----- 유니티 AppVersion 앱 버전 및 Build 자동화 공유 ------ | |
버튼을 누르면 Builds 폴더안에 "프로젝트이름_버전"로 빌드 세팅 기존의 내용대로 자동으로 되도록 하는 에디터 코드 공유 | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
namespace GameFrameworks | |
{ | |
[CreateAssetMenu (menuName = "App Version")] | |
public class AppVersion : ScriptableObject | |
{ | |
/// <summary> | |
/// The major version number. For versions that should replace a prior major version. | |
/// </summary> | |
[Header ("Semantic Version")] | |
[Min (0)] [SerializeField] private int major = 0; | |
/// <summary> | |
/// The minor version number. For versions that offer smaller improvements and additions. | |
/// </summary> | |
[Min (0)] [SerializeField] private int minor = 0; | |
/// <summary> | |
/// The patch version number. For versions that offer some kind of bug fix or incremental improvements. | |
/// </summary> | |
[Min (0)] [SerializeField] private int patch = 0; | |
/// <summary> | |
/// The type of release the software is intended for. | |
/// </summary> | |
[Header ("Release Suffix")] | |
[SearchableEnum] [SerializeField] private ReleaseType release = ReleaseType.Development; | |
/// <summary> | |
/// If an alpha or beta version, append this number to the end. | |
/// </summary> | |
[Min (0)] [SerializeField] private int releaseUpdate = 0; | |
/// <summary> | |
/// Creates a string in the semver format according to the set values in the inspector. | |
/// </summary> | |
/// <returns>The semver string of the current software being developed.</returns> | |
protected virtual string GetVersionNumber () | |
{ | |
string releaseSuffix = ""; | |
#if !UNITY_EDITOR_OSX && !UNITY_IOS | |
switch (release) | |
{ | |
case ReleaseType.Development: | |
releaseSuffix = "-dev"; | |
break; | |
case ReleaseType.Alpha: | |
releaseSuffix = "-a" + releaseUpdate.ToString (); | |
break; | |
case ReleaseType.Beta: | |
releaseSuffix = "-b" + releaseUpdate.ToString (); | |
break; | |
default: | |
break; | |
} | |
#endif | |
return string.Format ("{0}.{1}.{2}{3}", major, minor, patch, releaseSuffix); | |
} | |
/// <summary> | |
/// Implicitly cast this <c>AppVersion</c>s into a string for display in the UI. | |
/// </summary> | |
/// <param name="version">The object to cast.</param> | |
public static implicit operator string (AppVersion version) | |
{ | |
return version.GetVersionNumber (); | |
} | |
/// <summary> | |
/// Treat this object as a read only string of the current semver. | |
/// </summary> | |
/// <returns>The semver string.</returns> | |
public override string ToString () | |
{ | |
return GetVersionNumber (); | |
} | |
/// <summary> | |
/// Get current Bundle Version Code == releaseUpdate | |
/// </summary> | |
/// <returns>Bundle Version Code int.</returns> | |
public int GetBundleVersionCode() | |
{ | |
return releaseUpdate; | |
} | |
} | |
/// <summary> | |
/// Defines the various types of releases which dictate what sort of suffix is added to the end of the version string. | |
/// </summary> | |
public enum ReleaseType | |
{ | |
Development, | |
Alpha, | |
Beta, | |
Production | |
} | |
} | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEditor; | |
using System.IO; | |
using UnityEditor.Build.Reporting; | |
namespace GameFrameworks.CustomEditors | |
{ | |
[CustomEditor (typeof (AppVersion))] | |
public class AppVersionEditor : Editor | |
{ | |
string version = "v1.0.0"; | |
AppVersion instance; | |
public void OnEnable () | |
{ | |
instance = (AppVersion)target; | |
PlayerSettings.bundleVersion = instance; | |
} | |
public override void OnInspectorGUI() { | |
base.OnInspectorGUI(); | |
if (GUI.changed) { | |
PlayerSettings.bundleVersion = instance; | |
#if UNITY_IOS | |
PlayerSettings.iOS.buildNumber = instance.GetBundleVersionCode(); | |
#elif UNITY_ANDROID | |
PlayerSettings.Android.bundleVersionCode = instance.GetBundleVersionCode(); | |
#else | |
#endif | |
} | |
EditorGUILayout.Space(20); | |
serializedObject.Update(); | |
MainSettings(); | |
FooterInformation(); | |
serializedObject.ApplyModifiedProperties(); | |
} | |
void MainSettings() { | |
if (GUILayout.Button("빌드 ㄱㄱ")) | |
{ | |
BuildWithVersionName(); | |
} | |
} | |
void BuildWithVersionName() | |
{ | |
string version = instance.ToString(); | |
string productName = PlayerSettings.productName; | |
string outputFolder = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Builds", $"{productName}_{version}"); | |
string extension = ""; | |
BuildTarget buildTarget = EditorUserBuildSettings.activeBuildTarget; | |
switch (buildTarget) | |
{ | |
case BuildTarget.StandaloneWindows: | |
case BuildTarget.StandaloneWindows64: | |
extension = ".exe"; | |
break; | |
case BuildTarget.StandaloneOSX: | |
extension = ".app"; | |
break; | |
case BuildTarget.Android: | |
extension = ".apk"; | |
break; | |
} | |
string filePath = Path.Combine(outputFolder, productName + extension); | |
if (!Directory.Exists(outputFolder)) { | |
Directory.CreateDirectory(outputFolder); | |
} | |
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions | |
{ | |
scenes = EditorBuildSettingsScene.GetActiveSceneList(EditorBuildSettings.scenes), | |
locationPathName = filePath, | |
target = buildTarget, | |
options = BuildOptions.None | |
}; | |
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions); | |
if (report.summary.result == BuildResult.Succeeded) { | |
Debug.Log("Build succeeded: " + filePath); | |
OpenBuildFolder(outputFolder); | |
} else if (report.summary.result == BuildResult.Failed) { | |
Debug.LogError("Build failed"); | |
} | |
} | |
void OpenBuildFolder(string folderPath) | |
{ | |
if (Directory.Exists(folderPath)) { | |
#if UNITY_EDITOR_WIN | |
System.Diagnostics.Process.Start("explorer.exe", folderPath.Replace("/", "\\")); | |
#elif UNITY_EDITOR_OSX | |
System.Diagnostics.Process.Start("open", folderPath); | |
#endif | |
} else { | |
Debug.LogError("Build folder not found: " + folderPath); | |
} | |
} | |
void FooterInformation() { | |
EditorGUILayout.Space(); | |
EditorGUILayout.Space(); | |
GUILayout.BeginVertical("HelpBox"); | |
GUIStyle style = new GUIStyle(EditorStyles.label); | |
style.normal.textColor = Color.black; | |
style.fontSize = 16; | |
style.alignment = TextAnchor.MiddleCenter; | |
GUILayout.Label("SemoGame's Framework", style); | |
EditorGUILayout.Space(); | |
style.normal.textColor = Color.gray; | |
style.fontSize = 10; | |
GUILayout.Label("Version: " + version, style); | |
EditorGUILayout.Space(); | |
style.normal.textColor = Color.gray; | |
GUILayout.Label("Author: Curookie", style); | |
GUILayout.EndVertical(); | |
} | |
} | |
} | |
------ 특정 ScriptableObject 를 탭으로 만들고 싶으면 Inspector 뷰에서 ... 클릭 후 Properties 누르면 됨. ----- | |
사용용도 | |
- App Version.asset | |
- I2 Localize.asset | |
따로 빼놓으면 유용하다. | |
------ [중요] Unity Component 한줄로 컴포넌트 한번에 수정가능하게 하는 꿀팁 ------ | |
클래스 이름 위에 아래 한줄만 추가하면 됨. (using UnityEditor 에디터 코드이므로 분기 포함해야함, 빌드 시 에러남) | |
ex) | |
#if UNITY_EDITOR | |
[CanEditMultipleObjects] | |
#endif | |
public class Localize : MonoBehaviour { } | |
검색해서 대량수정할때 가능하면 기획자 좋아죽는다. | |
ex) i2 Localization Asset에 멀티 수정 기능이 안됨 -> 한줄 추가해서 가능하도록 변경해줌. | |
----- TMP Text 가변 레이아웃, 유니티 텍스트 동적 사이즈 원할 때 ---- | |
텍스트 내용에 맞게 uGUI의 크기를 바꾸고 싶을 때 있다. | |
구체적으로 방법 알려줌 대괄호는 [ 컴포넌트 와 세팅 ] | |
- 버튼들이 있는 팝업 [ Image ] | |
- [ Vertical Layout Group 또는 Horizontal Layout Group : 버튼 간의 여백 이나 스페이싱 주고싶으면 주면 됨 정렬방법도 원하는 대로 ] | |
- [ Content Size Fitter : Horizontal Fit - Preferred Size, Vertical Fit - Preferred Size ] | |
- 가변 감싸는 버튼과 이미지 [ Button ] , [Image] | |
- [ Vertical Layout Group 또는 Horizontal Layout Group : Control Child Size - Width, Height, Child Alignment - Middle Center, 여백 주고싶으면 Padding ] | |
- [ Content Size Fitter : Horizontal Fit - Preferred Size, Vertical Fit - Preferred Size ] | |
- 내용 오브젝트 [ TMP Text : Wrapping - Enable, Overflow - OverFlow ] | |
- [ Content Size Fitter : Horizontal Fit - Preferred Size, Vertical Fit - Preferred Size ] | |
핵심은 Content Size Fitter 를 Text 컴포넌트에 붙이고 원하는 동적 방향에 Preferred Size 옵션을 줘야하고, | |
그 상위 오브젝트에 Control Child Size 체크 된 Vertical Layout Group 이나 Horizontal Layout Group 을 붙이면 구현 가능. | |
------ i2 Localization Bug ----- | |
langCodes = LocalizationManager.GetAllLanguagesCode(); | |
langs = LocalizationManager.GetAllLanguages(); | |
Awake에서 참조할때 | |
langs.Count는 제대로 나오고 langCodes.Count 가 0으로 나옴 | |
------ await 가 비동기 끝날때까지 대기하는 건데 비동기 함수를 실행시키되 대기하지 않고 넘어가고 싶으면 .Forget() 을 붙이면 됨 ------ | |
public async UniTask InitQuest() { | |
model = Simulation.GetModel<PlatformerModel>(); | |
journal.UntrackQuests(); | |
await UniTask.Delay(2000); | |
await journal.ActivateQuest(quests[0]); | |
journal.TrackQuest(quests[0]); | |
MasterAudio.PlaySound("SFX_SE_SYS_MapMarker"); | |
} | |
여기선 비동기 대기를 진행하고 | |
QuestManager.Inst.InitQuest().Forget(); | |
GameManager.Inst.InitGame(); | |
이런식으로 하면 QuestManager가 끝날때까지 대기하지않고 비동기를 실행할 수 있음. | |
------ 공격 애니메이션 마다의 AttackTiming 공격 타이밍을 normal값으로 알면 그 시간 타이밍에 공격 계산을 하면 됨 ------ | |
Kinematic 공격 함수 공개함. | |
protected void ComputeAttack() { | |
if(attackDelayTimer > 0) { | |
// DBug.Log(attackDelayTimer); | |
attackDelayTimer -= Time.deltaTime; | |
// float _attackTimingNormal = ItemManager.Inst.weaponItems?.ElementAtOrDefault(0)?.GetWeaponInfo()?.GetAttackTiming ?? 0.137f; | |
ProcessAttackTiming(); | |
} else { | |
HandleAttackInput(); | |
} | |
UpdateAttackAnimSpeed(); | |
} | |
void ProcessAttackTiming() { | |
float _attackTimingNormal = ItemManager.Inst.weaponItems?.ElementAtOrDefault(0)?.GetWeaponInfo()?.GetAttackTiming ?? 0.588f; | |
if(attackDelayTimer <= totalAttackDelayTime*(1.0f-_attackTimingNormal) && !attackCalculated) { | |
attackCalculated = true; | |
var results = Physics2D.OverlapCircleAll(attackCollider2d.bounds.center, currentAttackRange * transform.localScale.x); | |
Vector2 playerDir = Vector3.left * (spriteRenderer.flipX ? 1 : -1); | |
int hitCount = ItemManager.Inst.weaponItems?.ElementAtOrDefault(0)?.currentMultiHitCount ?? 1; | |
var targets = results? | |
.Where(collider => collider.gameObject.tag == "Creature" && !collider.isTrigger) | |
.Where(collider => { | |
var dirNormal = (collider.transform.position - transform.position).normalized; | |
float dirAngle = Vector2.Angle(playerDir, dirNormal); | |
return dirAngle <= currentAttackAngle * 0.5f; | |
}) | |
.OrderBy(collider => Vector2.Distance(this.gameObject.transform.position, collider.transform.position)) | |
.Take(hitCount); | |
//.FirstOrDefault(); | |
//DAMAGE CALC | |
int _totalDamage = 0; | |
bool _isOnCritical = false; | |
if(Random.Range(0, 1000) < currentCriticalRate) { | |
_isOnCritical = true; | |
} else { | |
_isOnCritical = false; | |
} | |
_totalDamage = Mathf.RoundToInt(currentAttackPower * (_isOnCritical ? PLAYER_DEFAULT_CRITICAL_DAMAGE_FACTOR : 1.0f)); | |
if(targets!=null&&targets.Count()>0) { | |
foreach(var target in targets) { | |
int dir = ((this.transform.position.x < target.transform.position.x) ? 1 : -1); | |
var ev = Schedule<E_EnemyHurt>(); | |
ev.enemy = target.GetComponent<EnemyController>(); | |
ev.hurtDir = dir; | |
ev.isOnCritical = _isOnCritical; | |
ev.knockBackValue = currentAttackKnockBack; | |
ev.criticalKnockBackValue = currentCriticalAttackKnockBack; | |
ev.hurtDamage = _totalDamage; | |
} | |
} | |
} else if(attackDelayTimer <= PLAYER_AFTER_ATTACK_CONTROL_CHECK_TIME && !controlEnabled) { | |
// DBug.Log("ENABLE !!!!!!!!!!!"); | |
//AfterAttack ControlEnable | |
//TODO : BUG ABLE WHEN CRAFTING | |
controlEnabled = true; | |
} | |
} | |
void HandleAttackInput() { | |
if((InputManager.Inst?.input?.Player.Attack.IsPressed() ?? false) && !isJumpingAttacked) { | |
controlEnabled = false; | |
attackCalculated = false; | |
if(jumpState == JumpState.Jumping || jumpState == JumpState.PrepareToJump || jumpState == JumpState.InFlight) { | |
isJumpingAttacked = true; | |
} | |
//ATTACK ANIM | |
totalAttackDelayTime = 1f / currentAttackSpeed; | |
attackDelayTimer = totalAttackDelayTime; | |
// DBug.Log($"attackDelayTimer {attackDelayTimer} , currentAttackSpeed {currentAttackSpeed}"); | |
PlayAttackAnimation(); | |
} | |
} | |
void PlayAttackAnimation() { | |
if (ItemManager.Inst.weaponItems.Count > 0) { | |
switch (ItemManager.Inst.weaponItems[0]?.GetWeaponType()) { | |
case BlueprintType.Dagger : | |
if (DAGGER_MOTION) { | |
animator.SetTrigger("attack_dagger"); | |
DAGGER_MOTION = false; | |
} | |
else { | |
animator.SetTrigger("attack_dagger_2"); | |
DAGGER_MOTION = true; | |
} | |
break; | |
case BlueprintType.Sword : | |
if (SWORD_MOTION) { | |
animator.SetTrigger("attack_sword"); | |
SWORD_MOTION = false; | |
} | |
else { | |
animator.SetTrigger("attack_sword_2"); | |
SWORD_MOTION = true; | |
} | |
break; | |
case BlueprintType.Pickaxe : | |
animator.SetTrigger("attack_pickaxe"); | |
break; | |
default : | |
animator.SetTrigger("attack_default"); | |
break; | |
} | |
} else { | |
animator.SetTrigger("attack_default"); | |
} | |
} | |
------ 공격속도에 맞춰서 애니메이터 속도 변환하는 코드 ------- | |
void UpdateAttackAnimSpeed() { | |
var _stateInfo = animator.GetCurrentAnimatorStateInfo(0); | |
if(IsRunAttackAnim()) { | |
if(animator.speed != 1.0f) { return; } | |
float _clipLength = _stateInfo.length; | |
animator.speed = _clipLength / (1f / currentAttackSpeed); | |
// DBug.Log($"{GetCurrentClipName()} {_stateInfo.length}, _clipLength / (1f / currentAttackSpeed) {_clipLength / (1f / currentAttackSpeed)} "); | |
} else { | |
animator.speed = 1.0f; | |
// DBug.Log($"SPEED 1.0"); | |
} | |
} | |
bool IsRunAttackAnim() { | |
return GetCurrentClipName().Contains("Attack"); | |
} | |
string GetCurrentClipName() { | |
AnimatorClipInfo[] clipInfo = animator.GetCurrentAnimatorClipInfo(0); | |
if (clipInfo.Length > 0) { | |
return clipInfo[0].clip.name; | |
} | |
return string.Empty; | |
} | |
------ 물리 조작은 Kinematic 으로 직접 코드에서 처리 하는 편 ------ | |
------ Editor 코드 간단 정보 ------ | |
1. SerializedProperty 변수에 참조해서 할당할때 | |
OnEnable에서 할당하는게 좋음 한번만 하면 되니까 | |
if(m_sR_CurrentGraph?.objectReferenceValue == null ) { | |
m_sR_CurrentGraph.objectReferenceValue = _stats.transform.Find("CurrentGraph").GetComponent<SpriteRenderer>(); | |
} | |
serializedObject.ApplyModifiedProperties(); //이거 해줘야함. | |
2. OnInspectorGUI() 에서 | |
(m_tR_TargetMarkArea.objectReferenceValue as Transform).gameObject.SetActive(m_isOnTarget.boolValue); | |
이런식으로 조건에 따라 오브젝트 가져와서 끄게 제어가능 | |
3. OnInspectorGUI() 에서 | |
특정 변수가 변경시에 어떤 함수를 실행시키고 싶다면 | |
EditorGUI.BeginChangeCheck(); | |
EditorGUILayout.PropertyField(m_isFillGraph, new GUIContent("채색 여부")); | |
EditorGUILayout.IntSlider(m_lineThickness, 1, 30, new GUIContent("아웃라인 선 굵기")); | |
if (EditorGUI.EndChangeCheck()) { | |
OnValuesChanged(); | |
} | |
이런식으로 두개의 값중에 하나가 변경될때만 OnValuesChanged() 함수 실행 가능. | |
------ ArgumentException: Texture2D.GetPixels: texture data is either not readable, corrupted or does not exist. (Texture ) 에러 발생시 ----- | |
Read/Write 옵션 켰는데도 에러뜰 시 | |
Edit > Preferences > GI Cache 에서 캐시를 지우고 실행하면 잘 된다. | |
------ 유니티 단축키 ------ | |
Hierachy에서 | |
H == 숨기기 | |
Shift + H == 그 오브젝트 제외하고 다 숨기기 | |
Alt + Shift + A 오브젝트 비활성화 / 활성화 | |
------ 유니티에서의 MVC모델 설계 ------- | |
테트리스를 만들때 피스를 만드는 예를 들어서 설명함 | |
1. View_Piece 클래스 (View + Model) | |
- Mono 붙어있고 | |
- public SO_Piece piece; ScriptableObject로 Model을 대체함 | |
- public List<View_Block> blocks = new List<View_Block>(); 시작시 피스의 SO (디폴트 데이터)를 가져와서 수정될 수 있는 블럭을 만든다. | |
여기서 중요한 부분은 만약 데이터가 수정될 필요가 있을 시 (여기선 피스를 돌려서 위치값이 달라진다면) 런타임 도중 원본 데이터에 저장되지 않도록 설계해야함. | |
- Render 함수같은거 만들어서 갈아끼운다음 다시 랜더링하는 구조를 만들어줘야함 | |
2. SO_Piece 클래스 (Model) | |
- 데이터를 인스펙터에서 수정할 수 있도록 설계 | |
- 보통 SO_PieceEditor (에디터 클래스 하나 만들어서 기획자가 쉽게 에디터에서 데이터를 수정하도록 설계하도록 하면 더 좋음 ex 여기선 그리드 블럭클릭해서 피스모양을 만들 수 있게 한다든지) | |
3. PieceContoller 클래스 (Controller) | |
- View_Piece 를 포함하는 Prefab을 생성해서 pool로 관리하는 클래스 | |
- 엑셀에서 SO Export Import 하는 부분을 넣어도 되고 다른 매너저에서 관리해도 되는 부분 | |
핵심은 ScriptableObject들로 데이터를 생성하고 1개의 View_Piece 프리팹만 가지고 데이터만 갈아끼우는 식으로 구성이 가능하다. | |
데이터가 나뉘어져 있으니 Manager에서 엑셀로 데이터를 가져와서 SO를 생성하거나 반대로 생성된 SO를 가지고 엑셀로 넘겨주는 식으로 처리하면 더 좋다. | |
------ 이산 이벤트 시뮬레이션 패턴 (Discrete Event Simulation) ------ | |
시간의 흐름을 일정한 간격이 아닌 이벤트가 발생하는 순간에 따라 처리하는 시뮬레이션 방식 | |
이벤트는 대기열에 저장되며, 특정 시간이 되면 이벤트가 실행 | |
알아놓으면 매우 유용하다! (알기전과 알기후로 바뀔거다) | |
----- 프로그래밍적인 팁!! 리시버 구조를 사용하면 조금 더 복잡해 보이지만 역할분리 및 확장성을 높이고 유연하게 코드를 짤 수 있다. ----- | |
ex) 커맨드 패턴에서 리시버 없이 짜는거와 리시버를 사용해서 리시버에서 실행시키는 방식 | |
ex) 옵저버 패턴에서 리시버에서 실행시키는 형태도 마찬가지 | |
----- 에디터에서 ctrl+c 한것처럼 클립보드로 텍스트 저장시키고 싶을 때 ----- | |
TextEditor textEditor = new TextEditor | |
{ | |
text = "저장할 string 값" | |
}; | |
textEditor.SelectAll(); | |
textEditor.Copy(); | |
----- 평범한 해쉬 함수 SHA256 (string), 비용이 저렴한 FNV-1a (uint) 해쉬 함수 ------ | |
----- 다시한번 정리 MonoBehaviour 상속 스크립트와 Null 처리에 대한 간단한 팁 ------ | |
Mono 상속받은 클래스를 널 체크할때는 무조건 | |
if(MonoClass변수명) 으로 체크하고 | |
Mono 없는 클래스 일 경우 | |
if(Non-MonoClass변수명==null) 로 체크한다는 걸 명심해라. | |
----- Mathf.InverseLerp ------ 반대로 0에서 1 사이 값 필요할 경우 | |
-----[field:SerializeField] field 선언된 변수를 Editor 코드에서 가져오려면 <Power>k__BackingField 이런식으로 명확하게 정해줘야함. ----- | |
[field:SerializeField, Range(0, 31)] public int Power { get; set; } | |
Power = serializedObject.FindProperty("<Power>k__BackingField"); | |
----- Scene에서 수정할 수 있는 점 기즈모를 쓸려면 Editor 코드를 만들어줘야함 ----- | |
[CanEditMultipleObjects] | |
[CustomEditor(typeof(StatsView), true)] | |
public class StatsViewEditor : Editor | |
{ | |
string version = "v1.0.0"; | |
StatsView _view; | |
void OnEnable() | |
{ | |
_view = target as StatsView; | |
} | |
public override void OnInspectorGUI() { | |
base.OnInspectorGUI(); | |
EditorGUILayout.Space(20); | |
serializedObject.Update(); | |
MainDraw(); | |
FooterInformation(); | |
serializedObject.ApplyModifiedProperties(); | |
} | |
void OnSceneGUI() { | |
// maxPointPos 업데이트 | |
List<string> keys = new List<string>(_view.maxPointPos.Keys); | |
foreach (var key in keys) | |
{ | |
Vector3 newPosition = Handles.PositionHandle(_view.maxPointPos[key], Quaternion.identity); | |
if (_view.maxPointPos[key] != newPosition) | |
{ | |
Undo.RecordObject(_view, "Move Point"); | |
_view.maxPointPos[key] = newPosition; | |
EditorUtility.SetDirty(_view); | |
} | |
} | |
// centerPointPos 업데이트 | |
Vector3 newCenterPosition = Handles.PositionHandle(_view.centerPointPos.Item2, Quaternion.identity); | |
if (_view.centerPointPos.Item2 != newCenterPosition) { | |
Undo.RecordObject(_view, "Move Center Point"); | |
_view.centerPointPos = (_view.centerPointPos.Item1, newCenterPosition); | |
EditorUtility.SetDirty(_view); | |
} | |
} | |
private void MainDraw() { | |
EditorGUILayout.Space(20); | |
if (GUILayout.Button("점 위치 리셋")) { | |
_view.ResetRender(); | |
} | |
// if (GUILayout.Button("테스트 바운더리")) { | |
// // _weapon.TestRayPixels(); | |
// } | |
} | |
private void FooterInformation() { | |
EditorGUILayout.Space(); | |
EditorGUILayout.Space(); | |
GUILayout.BeginVertical("HelpBox"); | |
GUIStyle style = new GUIStyle(EditorStyles.label); | |
style.normal.textColor = Color.black; | |
style.fontSize = 16; | |
style.alignment = TextAnchor.MiddleCenter; | |
GUILayout.Label("SemoGame's Framework", style); | |
EditorGUILayout.Space(); | |
style.normal.textColor = Color.gray; | |
style.fontSize = 10; | |
GUILayout.Label("Version: " + version, style); | |
EditorGUILayout.Space(); | |
style.normal.textColor = Color.gray; | |
GUILayout.Label("Author: Curookie", style); | |
GUILayout.EndVertical(); | |
} | |
} | |
public class StatsView : MonoBehaviour | |
{ | |
public Weapon targetWeapon; | |
[SerializeField] | |
[SerializedDictionary] | |
public SerializedDictionary<string, Vector3> maxPointPos = new SerializedDictionary<string, Vector3>(); | |
public (string, Vector3) centerPointPos; | |
void OnEnable() { | |
if(maxPointPos.Count == 0) | |
{ | |
ResetRender(); | |
} | |
} | |
public void ResetRender() | |
{ | |
maxPointPos.Clear(); | |
maxPointPos.AddRange(new Dictionary<string, Vector3>() { | |
{ "Power", transform.TransformPoint(Vector2.up*100) }, | |
{ "Speed", transform.TransformPoint(Vector2.right*100) }, | |
{ "Balance", transform.TransformPoint(Vector2.left*100) }, | |
{ "Durability", transform.TransformPoint(Vector2.down*100 + Vector2.left*100) }, | |
{ "Stun", transform.TransformPoint(Vector2.down*100 + Vector2.right*100) }, | |
}); | |
centerPointPos = ("Center", transform.TransformPoint(Vector3.zero)); | |
EditorUtility.SetDirty(this); | |
} | |
private void OnDrawGizmos() | |
{ | |
Gizmos.color = Color.red; | |
foreach (var kvp in maxPointPos) | |
{ | |
Gizmos.DrawSphere(kvp.Value, 0.03f); | |
Handles.color = Color.red; | |
Handles.Label(kvp.Value + Vector3.up * 0.2f, kvp.Key); | |
} | |
Gizmos.color = Color.yellow; | |
Gizmos.DrawSphere(centerPointPos.Item2, 0.05f); | |
Handles.color = Color.yellow; | |
Handles.Label(centerPointPos.Item2 + Vector3.up * 0.2f, centerPointPos.Item1); | |
} | |
} | |
------ unity 에디터 코드 자동으로 오브젝트와 컴포넌트 있는지 체크하고 없으면 만드는 단순한 코드 (응용하시오) ----- | |
SerializedProperty sR_GuideArea; | |
SerializedProperty sR_Handle; | |
Weapon _weapon; | |
void OnEnable() | |
{ | |
_weapon = target as Weapon; | |
sR_GuideArea = serializedObject.FindProperty("sR_GuideArea"); | |
sR_Handle = serializedObject.FindProperty("sR_Handle"); | |
if(sR_GuideArea?.objectReferenceValue == null || sR_Handle?.objectReferenceValue == null) { | |
var _guideObj = _weapon.transform?.Find("GuideArea") ?? new GameObject("GuideArea").transform; | |
_guideObj.SetParent(_weapon.transform); | |
_guideObj.localPosition = Vector3.zero; | |
_guideObj.localScale = Vector3.one; | |
SpriteRenderer _guideSR; | |
_guideSR = _guideObj.TryGetComponent<SpriteRenderer>(out _guideSR) ? _guideSR : _guideObj.AddComponent<SpriteRenderer>(); | |
var _handleObj = _weapon.transform?.Find("HandleArea") ?? new GameObject("HandleArea").transform; | |
_handleObj.SetParent(_weapon.transform); | |
_handleObj.localPosition = Vector3.zero; | |
_handleObj.localScale = Vector3.one; | |
SpriteRenderer _handleSR; | |
_handleSR = _handleObj.TryGetComponent<SpriteRenderer>(out _handleSR) ? _handleSR : _handleObj.AddComponent<SpriteRenderer>(); | |
sR_GuideArea.objectReferenceValue = _guideSR; | |
sR_Handle.objectReferenceValue = _handleSR; | |
serializedObject.ApplyModifiedProperties(); | |
// _weapon.InitShapeOfPixelData(); | |
} | |
} | |
------ Render 하는 코드 짤때 꿀팁 ------ | |
Render할 데이터의 Hash값을 가지고 합산이나 식을 만든다음 그 값이 달라질때만 렌더하게 하면 비용을 엄청 줄일 수 있다. | |
update문에 때려박는 무식한 짓 하지않아도 됨. | |
------ unity sprite.bounds 와 sprite.rect 차이 ------ | |
bounds 는 world 공간 기반 3D, 크기 회전 고려 | |
rect은 텍스쳐 공간 기반 2D, 어떠한 변환도 고려안하고 텍스쳐 내에서의 스프라이트의 원래 크기 | |
------ serializedObject.FindProperty("sR_Sprite"); 로 [SerializedField] private 변수 도 가져와서 할당 가능하다. 이때 주의할 점은 | |
serializedObject.ApplyModifiedProperties(); 를 꼭 해야함 ------ | |
ex) | |
public class Metal | |
{ | |
[SerializeField] SpriteRenderer sR_Sprite; | |
[SerializeField] BoxCollider2D bC_Collider; | |
} | |
에디터 코드 | |
[CanEditMultipleObjects] | |
[CustomEditor(typeof(Metal), true)] | |
public class MetalEditor : Editor | |
{ | |
void OnEnable() | |
{ | |
metal = target as Metal; | |
sR_Sprite = serializedObject.FindProperty("sR_Sprite"); | |
bC_Collider = serializedObject.FindProperty("bC_Collider"); | |
sR_Sprite.objectReferenceValue = metal.GetComponent<SpriteRenderer>(); | |
bC_Collider.objectReferenceValue = metal.GetComponent<BoxCollider2D>(); | |
serializedObject.ApplyModifiedProperties(); | |
metal.InitShapeOfPixelData(); //뭔가 Init 코드가 있다면 할당한 거 Apply 하고 이후에 진행! | |
} | |
------ Android에서는 백그라운드로 간다음 드래그해서 끄는경우가 많은데 이럴땐 Quit()인지가 안돼서 Pause() 함수에서 처리하는게 맞다. ------ | |
------ 모바일 아주 큰 함정 DontDestroyOnLoad 스크립트(싱글톤 매니저 같은) 붙은 오브젝트에 OnApplicationQuit 함수를 넣으면 실행 안됨!! ----- | |
------ 모바일 환경에서 꿀팁 OnEnable OnDisable 함수로 이벤트 함수 관리하는게 맞다. Awake Start 에 넣으면 누수될 가능성이 있고 실제로 누수된 경우 있다. ------- | |
------ await 에서 Func<Task> 가 null 인지 체크해서 널일 경우 넘어가게 해야한다. ------ | |
await (afterFunc?.Invoke() ?? Task.CompletedTask); | |
------ UniTask 비동기 유용한 기능들 정리 ----- | |
이런 경우가 있다. 기존에 코루틴(Coroutine) 으로 구현해놨던걸 UniTask로 큰 코드 변경 비용없이 구현할수 있다. | |
(*주의 상위 함수에서 Task가 아닌 UniTask로 받아야한다.) | |
public IEnumerator ResultAnimRun() { } 이런 코루틴 함수가 있으면 | |
.ToUniTask() 를 붙여주면 끝. | |
ex) | |
public override async UniTask ShowUI() //Task가 아닌 UniTask로 받아야한다는 의미는 여기! | |
{ | |
await ResultAnimRun().ToUniTask(); | |
} | |
// 코루틴을 StopCoroutine 정지하는 것까지 고려해서 UniTask로 변환하고 싶을때 | |
currentTask = RunRewardAnim().ToUniTask(cancellationToken: cts.Token); | |
코루틴에서 IEnumerator 변수 만들어놓고 실행시에 저장한다음 실행되고있는 코루틴 체크하는 역할을 하는 부분을 | |
cts = new CancellationTokenSource(); | |
cts가지고 현재 돌고있는지 체크해서 중단하는것 가능. | |
혹은 currentTask = RunRewardAnim().ToUniTask(); | |
UniTask 변수를 만들어놓고 그 안에 if (currentTask.HasValue && !currentTask.Value.Status.IsCompleted()) { | |
이런식으로 체크가능 (사실상 체크만 가능하므로 cts로 처리하는게 맞다.) | |
// 현재 실행 중인 작업이 있으면 취소 | |
if (cts != null) | |
{ | |
cts.Cancel(); | |
cts.Dispose(); | |
cts = null; | |
} | |
// 새로운 CancellationTokenSource 생성 | |
cts = new CancellationTokenSource(); | |
try | |
{ | |
// RunRewardAnim 작업 시작 | |
await RunRewardAnim(cts.Token); | |
} | |
catch (OperationCanceledException) | |
{ | |
Debug.Log("Operation was cancelled"); | |
} | |
finally | |
{ | |
cts.Dispose(); | |
cts = null; | |
} | |
어떤 비동기 실행중일때 취소하는 기능 | |
var cts = new CancellationTokenSource(); | |
cts.Cancel(); // 취소 이벤트 버튼같은데 넣자. | |
await UnityWebRequest.Get("http://google.co.jp").SendWebRequest().WithCancellation(cts.Token); //네트워크에서 뭘 받아올때 | |
await UniTask.DelayFrame(1000, cancellationToken: cts.Token); // 10초기다리다가 취소처리 | |
어떤 비동기 몇초 지나면 취소하는 기능 | |
var cts = new CancellationTokenSource(); | |
cts.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 5초 시간초과 | |
try | |
{ | |
await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(cts.Token); | |
} | |
catch (OperationCanceledException ex) | |
{ | |
if (ex.CancellationToken == cts.Token) | |
{ | |
Debug.Log("Timeout"); | |
} | |
} | |
//두개 다 섞어서 사용하는 예시 | |
var cancelToken = new CancellationTokenSource(); | |
cancelButton.onClick.AddListener(()=> | |
{ | |
cancelToken.Cancel(); // 버튼 클릭시 취소 | |
}); | |
var timeoutToken = new CancellationTokenSource(); | |
timeoutToken.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 5초경과시 취소 | |
try | |
{ | |
// combine token | |
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, timeoutToken.Token); | |
await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(linkedTokenSource.Token); | |
} | |
catch (OperationCanceledException ex) | |
{ | |
if (timeoutToken.IsCancellationRequested) | |
{ | |
Debug.Log("Timeout."); | |
} | |
else if (cancelToken.IsCancellationRequested) | |
{ | |
Debug.Log("Cancel clicked."); | |
} | |
} | |
// 로딩 구현 | |
동시처리를 하려면 | |
public async UniTaskVoid LoadManyAsync() | |
{ | |
//전부 완료되면 다음으로 넘어감 | |
var (a, b, c) = await UniTask.WhenAll( | |
LoadAsSprite("foo"), | |
LoadAsSprite("bar"), | |
LoadAsSprite("baz")); | |
} | |
async UniTask<Sprite> LoadAsSprite(string path) | |
{ | |
var resource = await Resources.LoadAsync<Sprite>(path); | |
return (resource as Sprite); | |
} | |
UniTask로 3번 클릭 구현 | |
async UniTask TripleClick() | |
{ | |
// 기본적으로 button.GetCancellationTokenOnDestroy를 사용하여 비동기의 수명을 관리합니다. | |
await button.OnClickAsync(); | |
await button.OnClickAsync(); | |
await button.OnClickAsync(); | |
Debug.Log("3번 클릭"); | |
} | |
// 가장 많이 쓸 수 있는 일정 시간이후 스킵가능하게 하는 비동기 | |
using Cysharp.Threading.Tasks; | |
using System.Threading; | |
using UnityEngine; | |
public class SkippableDelayExample : MonoBehaviour | |
{ | |
private CancellationTokenSource delayCancellationTokenSource; | |
private CancellationTokenSource activationCancellationTokenSource; | |
void Start() | |
{ | |
// 예제 시작 시 딜레이를 시작 | |
StartSkippableDelay(5, 1).Forget(); | |
} | |
private async UniTaskVoid StartSkippableDelay(float totalDelayInSeconds, float skipActivationDelayInSeconds) | |
{ | |
// 새로운 CancellationTokenSource를 생성합니다. | |
delayCancellationTokenSource = new CancellationTokenSource(); | |
activationCancellationTokenSource = new CancellationTokenSource(); | |
// 일정 시간 후에 CancellationToken을 활성화합니다. | |
activationCancellationTokenSource.CancelAfterSlim((int)(skipActivationDelayInSeconds * 1000)); | |
try | |
{ | |
// 지연 실행을 시작합니다. | |
await UniTask.Delay((int)(totalDelayInSeconds * 1000), cancellationToken: delayCancellationTokenSource.Token); | |
// 지연이 완료된 후 실행할 작업을 여기에 추가합니다. | |
Debug.Log("Delay completed"); | |
} | |
catch (OperationCanceledException) | |
{ | |
// 지연이 취소되었을 때 실행할 작업을 여기에 추가합니다. | |
Debug.Log("Delay skipped"); | |
} | |
finally | |
{ | |
// CancellationTokenSource를 정리합니다. | |
delayCancellationTokenSource.Dispose(); | |
activationCancellationTokenSource.Dispose(); | |
} | |
} | |
void Update() | |
{ | |
// 예제: 스페이스 키를 눌러 지연을 스킵합니다. | |
if (Input.GetKeyDown(KeyCode.Space)) | |
{ | |
SkipDelay(); | |
} | |
} | |
private void SkipDelay() | |
{ | |
// skipActivationDelayInSeconds 후에만 지연을 취소할 수 있게 합니다. | |
if (activationCancellationTokenSource.IsCancellationRequested) | |
{ | |
delayCancellationTokenSource.Cancel(); | |
} | |
} | |
} | |
yield return new WaitForSeconds/WaitForSecondsRealtime await UniTask.Delay | |
yield return null await UniTask.Yield | |
await UniTask.NextFrame | |
yield return WaitForEndOfFrame await UniTask.WaitForEndOfFrame | |
new WaitForFixedUpdate await UniTask.WaitForFixedUpdate | |
yield return WaitUntil await UniTask.WaitUntil | |
------ : 콜론으로 매개변수 지정하기 가능 ------ | |
public static void M(int a = 1, int b = 2, int c = 3) { } | |
public static void Main() | |
{ | |
M(); //M(1,2,3) | |
M(1, c: 2, b: 1); // 콜론(:) 을 이용해서 값을 전할 매개변수를 고를 수 있다. | |
} | |
------ UniTask DoTween과 같이 쓸려면 player Scripting Define Symbols에 UNITASK_DOTWEEN_SUPPORT 넣어줘야함. ------- | |
------ gameObject 비활성화 체크 할때 주의할 점 ------ | |
GameObject.activeSelf 로 체크하지말고 GameObject.activeInHierarchy 로 체크해야 실제 게임뷰에서 비활성화 된 걸 체크할 수 있다. | |
SetActive(true)이여도 부모가 false라면 실제는 보이지않는데 activeSelf는 true를 반환한다! | |
------ 테스트 모드같이 특정 코드를 활성화하고 비활성화 해야할 경우가 상당히 많은데 ----- | |
#if UNITY_EDITOR | |
Debug.Log(log); | |
#endif | |
이런 전처리기 사용만 하지말고 Conditional 사용하여 Unity > Project Settings 에서 Symbols 등록후 아래와 같이 사용하면 좋다. | |
[System.Diagnostics.Conditional("TEST_MODE_1")] | |
public static void Log(string log) | |
{ | |
Debug.Log(log); | |
} | |
이러면 TEST_MODE_1 이면 실행가능 | |
------ Scriptable Object가 Inspector 에디터에선 수정됬는데 .asset 파일 데이터가 저장 안 될때 | |
EditorUtility.SetDirty(this); 추가해줘야한다. | |
#if UNITY_EDITOR | |
public void Save() { | |
string thisFileNewName = (_slug.Trim() == "") ? $"STAGE{_level.ToString("D4")}" : $"STAGE{_level.ToString("D4")}___{_slug}"; | |
string assetPath = AssetDatabase.GetAssetPath(this.GetInstanceID()); | |
AssetDatabase.RenameAsset(assetPath, thisFileNewName); | |
EditorUtility.SetDirty(this); | |
AssetDatabase.SaveAssets(); | |
} | |
#endif | |
----- Easy Save 3 ------ | |
KeyValuePair 저장 안됨. | |
------ 2022.3.10f1 유니티 기준 TextMeshPro Text (UI) 동적으로 Material Preset 변경하는방법 ------ | |
기존에 인터넷 정보, GPT 다 틀림. | |
GameObject txt = new GameObject("txt"); | |
txt.transform.SetParent(buttonObj.transform); | |
TextMeshProUGUI textComp = txt.AddComponent<TextMeshProUGUI>(); | |
textComp.text = (1 + i) + ""; | |
textComp.fontSize = 60; | |
textComp.alignment = TextAlignmentOptions.Center; | |
textComp.UpdateFontAsset(); | |
textComp.fontSharedMaterial = albumUI.m_OutlineText; | |
textComp.color = Color.white; | |
txt.transform.localPosition = Vector3.zero; | |
txt.transform.localScale = Vector3.one; | |
txt.transform.localEulerAngles = Vector3.zero; | |
textComp.SetAllDirty(); | |
textComp.UpdateFontAsset(); // textComp.font = Resources.Load<TMP_FontAsset>("Fonts/Dovemayo_gothic SDF"); | |
textComp.fontSharedMaterial = albumUI.m_OutlineText; | |
textComp.SetAllDirty(); | |
이 3가지가 중요. | |
font으로 Font Asset 설정을 반드시 있어야함. (동적으로 생성했기에 UpdateFontAsset 함수로 자동 디폴트 Font Asset을 생성한 상황) | |
Material Preset은 fontSharedMaterial으로 변경가능. | |
마지막에 SetAllDirty()로 설정사항 저장. | |
------ [중요 팁] 변수명 앞에 mono 클래스나 프레임워크 클래스들을 나타내는 키워드를 붙이는 방식. ---- | |
익숙해진다면 상당히 직관적이고 변수명을 줄이기에도 좋은 방안 | |
ex) public ParticleSystem pS_ItemEffect; | |
1. t = Text | |
2. tT = TMP_Text | |
3. tR = Transfrom | |
4. tO = Toogle | |
5. tC = TMP_Counter (내가만든 프레임워크 클래스) | |
6. i = Image | |
7. b = Button | |
8. c = Canvas | |
9. cG = CanvasGroup | |
10. cA = Camera | |
----- DOTween DOPath SetEase 패스 부분적으로 먹는게 아니라 전체 패스에서 먹게하는 방법 ------ | |
i_Main.transform.DOPath(path, 0.6f, PathType.CatmullRom) | |
.SetOptions(false, AxisConstraint.None, AxisConstraint.None) | |
.SetEase(Ease.OutQuart)) | |
SetOptions로 SetOptions(false, AxisConstraint.None, AxisConstraint.None) 옵션 넣어주면 패스 전체에 대한 Ease값을 줄 수 있다. | |
------ 파티클시스템Particle System 스크립트로 완전히 즉시 끄는방법 ------ | |
ps.Stop(); | |
ps.Clear(); 클리어까지 하면 즉시 끌수 있다. | |
------ Sequence DoTween 팁 ------- | |
특정 시점으로 이동 Goto(시간, true); | |
플레이 된 시간 체크 Elapsed(); | |
------ 키보드 처리 뉴인풋 ------ | |
Keyboard.current.gKey.wasPressedThisFrame = G키 누르면 | |
------ Stencil 사용하려면 URP가 활성화 되어있어야하고 Stencil 마스크로 마스크안에 마스크 복합적인 마스킹 구성이 가능하다.------ | |
uGUI Rect Mask 2D 는 스텐실 방식이 아니므로 Stencil Mask랑 같이 쓸 수 있다. | |
uGUI Mask 는 자식 오브젝트 모두에게 stencil id 1 alaways equal 255/ 255 값으로 씌워버림. | |
마스크 1 ref 1, 마스크 2 ref 2, 그룹 마스크 ref 3 이런식으로 마스크 안에 마스킹 가능 | |
----- Shader Ref (Stencil ID) 의미 | |
유니티 기본 1 | |
약속같은 거 | |
----- Shader Comp (Stencil Comparison) 의미 | |
0 (Disabled) (Depth와 Stencil 테스트를 안함) | |
1 (Never) 항상 실패. (비교 연산이 항상 실패함) | |
2 (Less) Ref 값이 스텐실 버퍼의 값보다 작은 경우 통과. | |
3 (Equal) Ref 값이 스텐실 버퍼의 값과 같은 경우 통과. | |
4 (LessEqual) Ref 값이 스텐실 버퍼의 값보다 작거나 같은 경우 통과. | |
5 (Greater) Ref 값이 스텐실 버퍼의 값보다 큰 경우 통과. | |
6 (NotEqual) Ref 값이 스텐실 버퍼의 값과 다른 경우 통과. | |
7 (GreaterEqual) Ref 값이 스텐실 버퍼의 값보다 크거나 같은 경우 통과. | |
8 (Always) 항상 통과. (비교 연산을 생략하고 항상 통과함) | |
---- Shader Pass (Stencil Operation) 의미 | |
0 (Keep) 현재 스텐실 값을 그대로 유지합니다. | |
1 (Zero) 스텐실 값을 0으로 설정합니다. | |
2 (Replace) 현재 스텐실 값을 참조 값(Ref)으로 대체합니다. | |
3 (IncrSat) 스텐실 값을 1씩 증가시키며, 값이 최대값(255)을 초과하지 않도록 포화 연산을 수행합니다. | |
4 (DecrSat) 스텐실 값을 1씩 감소시키며, 값이 최소값(0) 아래로 내려가지 않도록 포화 연산을 수행합니다. | |
5 (Invert) 스텐실 값을 비트 단위로 반전시킵니다. | |
6 (IncrWrap) 스텐실 값을 1씩 증가시키며, 값이 최대값을 초과하면 0으로 래핑합니다. | |
7 (DecrWrap) 스텐실 값을 1씩 감소시키며, 값이 최소값 아래로 내려가면 최대값(255)으로 래핑합니다. | |
----- Shadar ColorMask ------ | |
R (Red) | |
G (Green) | |
B (Blue) | |
A (Alpha) | |
각 채널은 하나의 비트로 표현되며, ColorMask 속성은 4비트 값 | |
0000 (0): 아무것도 렌더링하지 않음. | |
0001 (1): 알파 채널만 활성화. | |
0010 (2): 블루 채널만 활성화. | |
0100 (4): 그린 채널만 활성화. | |
1000 (8): 레드 채널만 활성화. | |
0011 (3): 블루와 알파 채널 활성화. | |
0101 (5): 그린과 알파 채널 활성화. | |
0110 (6): 그린과 블루 채널 활성화. | |
0111 (7): 그린, 블루, 알파 채널 활성화. | |
1001 (9): 레드와 알파 채널 활성화. | |
1010 (10): 레드와 블루 채널 활성화. | |
1011 (11): 레드, 블루, 알파 채널 활성화. | |
1100 (12): 레드와 그린 채널 활성화. | |
1101 (13): 레드, 그린, 알파 채널 활성화. | |
1110 (14): 레드, 그린, 블루 채널 활성화. | |
1111 (15): 모든 채널 활성화 (기본값). RGBA 라고 쓰기도함 | |
------ Custom Shader 코드로 짰을때 uGUI Mask나 Rect Mask2D에서 마스킹 안됨 이때는 ------- | |
Properties { | |
_StencilComp("Stencil Comparison", Float) = 8 | |
_Stencil("Stencil ID", Float) = 0 | |
_StencilOp("Stencil Operation", Float) = 0 | |
_StencilWriteMask("Stencil Write Mask", Float) = 255 | |
_StencilReadMask("Stencil Read Mask", Float) = 255 | |
_ColorMask("Color Mask", Float) = 15 | |
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0 | |
} | |
SubShader | |
{ | |
Tags { "Queue"="Transparent" "RenderPipeline"="UniversalPipeline" "RenderType"="Transparent" } | |
Pass | |
{ | |
Stencil | |
{ | |
Ref [_Stencil] | |
Comp [_StencilComp] | |
Pass [_StencilOp] | |
ReadMask [_StencilReadMask] | |
WriteMask [_StencilWriteMask] | |
} | |
ColorMask [_ColorMask] | |
Blend SrcAlpha OneMinusSrcAlpha | |
ZTest [unity_GUIZTestMode] | |
} | |
파이프라인이 URP여서 그럴거고, 의무적으로 위 부분을 넣어 줘야함. | |
------ 프리팹 마다, 메터리얼의 텍스쳐나 내용을 다르게 해야할 경우 Material Property Block 기능을 이용하면 됨. ------- | |
----- 쉐이더 코드 URP 일때 ----- | |
Shader error in 'Custom/DesaturateMaskedArea': redefinition of '_Time' at 이런 에러 날 경우 #include 안 될때 | |
CGPROGRAM ... ENDCG 라고 되어있다면 HLSLPROGRAM ... ENDHLSL | |
------ DoTween DOFade 쓸때 or AddComponent<TMP_Text> 라고 쓸때 TMP_Text 말고 TextMeshProUGUI로 처리해야 에러 안남 ------- | |
------ Firebase Unity Custom Event 안됨 ------ | |
FirebaseAnalytics.LogEvent("stage_start_event", "stage", currentStage.GetLevel()); 이런식으로 커스텀 이벤트 만들면 매개변수 부분(stage, currentStage~~)이 Firebase에서 안보임 그냥 설정된 값 쓰셈 | |
------ 에디터 코드 -------- | |
bool showMainSettings = true; | |
public override void OnInspectorGUI() { | |
EditorStyles.foldout.fontStyle = FontStyle.Bold; | |
showMainSettings = EditorGUILayout.Foldout(showMainSettings, new GUIContent("Main Settings"), true); | |
EditorStyles.foldout.fontStyle = FontStyle.Normal; | |
} | |
폴딩 가능하게 만들수 있음. + 굵은 표기 스타일 예시 | |
GUIStyle customStyle = EditorStyles.foldout; | |
FontStyle prevStyle = customStyle.fontStyle; | |
customStyle.fontStyle = FontStyle.Bold; | |
showStageList = EditorGUILayout.Foldout(showStageList, new GUIContent("스테이지 그룹 설정"), customStyle); | |
customStyle.fontStyle = prevStyle; | |
폴드 하는 라벨만 굵게 하고 다시 원래 폰트 스타일로 돌리는 코드 | |
------ Camera 연동 Canvas Camera.main.ScreenToWorldPoint() 할때 z값 동적으로 찾아오기 위한 부모 캔버스 찾는 코드 하나 짜놓으면 좋음. | |
아래 코드로 부모 캔버스 찾아서 | |
if (isOverlayCanvas) touchPosWorld = input.point.action.ReadValue<Vector2>(); | |
else touchPosWorld = Camera.main.ScreenToWorldPoint(new Vector3(touchPos.x, touchPos.y, GetTopmostCanvas(this.gameObject).planeDistance)); | |
이런식으로 오버레이캔버스일경우랑 아닐경우 예외처리 해놓을수 있다. | |
private Canvas GetTopmostCanvas(GameObject component) { | |
Canvas[] parentCanvases = component.GetComponentsInParent<Canvas>(); | |
if (parentCanvases != null && parentCanvases.Length > 0) { | |
return parentCanvases[parentCanvases.Length - 1]; | |
} | |
return null; | |
} | |
------- NEW INPUT Touch 관련 ------- | |
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch; | |
Input.Mouse | |
Touch.activeFingers[0].currentTouch.phase.Equals(TouchPhase.Began) | |
Touch.activeFingers[0].currentTouch.phase.Equals(TouchPhase.Ended) | |
------- await async 비동기 안에서 에러나면 로깅안되니까 try catch 꼭 써서 예외처리해줘야함! ------- | |
------- firebase function 사용하면 서버없이 firebase 명령어 실행가능 Blaze 요금제로 올려야함! ------ | |
vscode로 cli 설치가능 | |
------ firebase realtime database ------ | |
Rule 정하기 | |
".validate": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 1000" | |
키 값으로 내용 그대로 - "key" : | |
주로 유저아이디 $uid : 이런식으로 변수를 만들수 있음. | |
newData 는 value의 내용들어오는 값을 의미하고 | |
$uid === 같다는 표현은 3개 | |
Cloud Functions 사용 | |
Firebase Cloud Functions을 사용하여 랭킹 시스템의 로직을 서버 측에서 처리할 수 있습니다. | |
예를 들어, 사용자의 점수가 업데이트될 때마다 Cloud Function이 트리거되어 랭킹을 재계산하고, | |
필요한 경우에만 랭킹 정보를 업데이트합니다. 이 방법은 클라이언트 측의 작업 부담을 줄이고, | |
필요한 경우에만 데이터베이스를 업데이트하여 비용을 절감할 수 있습니다. | |
랭킹 캐싱 | |
랭킹 정보를 서버 측에서 정기적으로 계산하고, 계산된 랭킹 정보를 캐시에 저장합니다. | |
사용자가 자신의 랭킹을 조회할 때는 캐시된 정보를 제공합니다. | |
이 방법은 데이터베이스의 읽기 작업을 줄이고, 사용자에게 빠른 응답 시간을 제공할 수 있습니다. | |
분산 카운터 사용 | |
매우 높은 동시성을 처리해야 하는 경우(예: 동시에 수천 명의 사용자가 점수를 업데이트하는 경우), | |
분산 카운터를 사용하여 쓰기 작업의 부하를 여러 노드에 분산시킬 수 있습니다. | |
이는 Firebase Realtime Database의 특정 한계를 극복하고, 시스템의 확장성을 높이는 방법입니다. | |
------ 토스트 메시지 안드로이드 띄우기 ------ | |
/// <param name="message">Message string to show in the toast.</param> | |
private void _ShowAndroidToastMessage(string message) | |
{ | |
AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); | |
AndroidJavaObject unityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"); | |
if (unityActivity != null) | |
{ | |
AndroidJavaClass toastClass = new AndroidJavaClass("android.widget.Toast"); | |
unityActivity.Call("runOnUiThread", new AndroidJavaRunnable(() => | |
{ | |
AndroidJavaObject toastObject = toastClass.CallStatic<AndroidJavaObject>("makeText", unityActivity, message, 0); | |
toastObject.Call("show"); | |
})); | |
} | |
} | |
아래 clipboard랑 잘 활용하면 좋음. | |
------ Android / iOS 클립보드 처리 할때 유용한 플러그인 ------ | |
github에 공유된 UniClipboard 사용하자. | |
토스트 메시지는 안 띄워주니 직접 구현하자. | |
------ Google Simple Sign in 에셋 Scope Invalid 400 Error 해결법 (2024.03 기준)------ | |
다 필요없고 GoogleAuthSettings.asset 리소츠 파일에 있는 저녀석의 Access Scopes 에 email 이랑 profile만 넣어라 대소문자도 똑같이 | |
그리고 UserInfo.cs 에 profile 변수 추가해놓으셈 항목은 | |
앱 등록 수정 > 에 두번째 페이지에 리스트 나옴 | |
.../auth/userinfo.email 기본 Google 계정의 이메일 주소 확인 | |
.../auth/userinfo.profile 개인정보(공개로 설정한 개인정보 포함) 보기 | |
이런식으로 스코프 데이터가 맞지 않아서 발생하는 에러임 | |
------ APP Icon 해상도나 상황별로 512*512짜리 원본하나로 나눠주는 사이트에서 나눈다음 Build Settings 의 Icon에서 변경하면 됨. ------ | |
여기 꽤 좋음. android ldpi 만 안나오고 나머진 다 나옴.(unity 2022.3.19f1) 기준 https://makeappicon.com/ | |
------ 안드로이드 SDK 경로 다른 유니티 버전으로 옮길시 이부분만 옮기면 다시 설치할 필요없음 ------ | |
C:\Program Files\Unity\Hub\Editor\2022.3.10f1\Editor\Data\PlaybackEngines\AndroidPlayer\SDK\platforms | |
Data\PlaybackEngines 이 부분만 외우면 나머진 쉽게 찾아감 안에 있는 android-33 이런거 옮기면 됨. (33버전임) | |
------ async void Start() 유니티 이벤트함수도 async await 지원한다. ------ | |
------ Sign key 와 Upload key 둘 다 제공해야 Cloud API에 두개다 제공되어야함. GPGS 테스터 아닌 일반유저도 작동된다. ------- | |
------- Google Cloud Oauth2.0 Test 다가 Publish 로 바꾸면 API ID가 변경되서 GPGS 다시 등록해야함!! 기억하셈! ------------- | |
------ 사운드 재생중 사운드 세팅 변경시 사운드 미출력 ----- | |
Unity 작업중 이나 빌드에서 사운드 세팅 변경(ex 스피커 <-> 블루투스)시 발생하는 이슈 | |
AudioSettings.OnAudioConfigurationChanged += (changed) => // changed : 장치 변경으로 인한 것이면 true | |
{ | |
Debug.Log("Change!"); // 사운드 로직 구성 | |
}; | |
------- 원스토어 인앱결제 API V7 One Store IAP API V7 (SDK V21) 심각한 문제 Unity SDK 해결방안 (2024.03. Unity Android 2022.3.10f1 기준) ------ | |
이 글은 원스토어 인앱결제 SDK로 고통받는 개발자가 있을 수 있기에 남깁니다. | |
Sample 프로젝트 제가 명시한 버전에서 작동 안됨. | |
Auth과 IAP는 초기화 코드에 접근은 하나 그 이후 콜백 아무 반응 없음. 라이센스도 안 되는걸로 판단. 디버그를 찍어도 안뜸. | |
현재 글 쓴 시점에서 해결방안이 없기에 V6 (SDK V19) - 정확한 버전 iap_v19_unity_plugin-v1.1.0.unitypackage 을 사용해서 구현했을 때 문제 없이 작동했습니다. | |
빌드 제출 하려면 SDK V21이상 무조건 써야해서 해결방안을 찾다가 찾아낸 해법은 | |
proguard 랑 R8 release 사용 안하도록 설정 체크 해제하고 | |
AndroidManifest.xml에 파이어베이스 pushMessaging에서 android:exported="true" 이부분 추가했음. | |
ex) | |
<?xml version="1.0" encoding="utf-8"?> | |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
package="com.Deliciousgames.Fourtris1" | |
android:versionCode="1" android:versionName="1.0"> | |
<uses-permission android:name="android.permission.INTERNET" /> | |
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | |
<!-- | |
if your binary use ONE store's In-app SDK, | |
Please make sure to declare the following query on Androidmanifest.xml. | |
Refer to the notice for more information. | |
https://dev.onestore.co.kr/devpoc/support/news/noticeView.omp?noticeId=32968 | |
--> | |
<queries> | |
<intent> | |
<action android:name="com.onestore.ipc.iap.IapService.ACTION" /> | |
</intent> | |
<intent> | |
<action android:name="android.intent.action.VIEW" /> | |
<data android:scheme="onestore" /> | |
</intent> | |
</queries> | |
<application android:label="@string/app_name" android:icon="@drawable/app_icon"> | |
<!-- The MessagingUnityPlayerActivity is a class that extends | |
UnityPlayerActivity to work around a known issue when receiving | |
notification data payloads in the background. --> | |
<activity android:name="com.google.firebase.MessagingUnityPlayerActivity" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" android:exported="true"> | |
<!-- <activity android:name="com.unity3d.player.UnityPlayerActivity" | |
android:theme="@style/UnityThemeSelector" | |
android:exported="true"> --> | |
<intent-filter> | |
<action android:name="android.intent.action.MAIN" /> | |
<category android:name="android.intent.category.LAUNCHER" /> | |
</intent-filter> | |
<meta-data android:name="unityplayer.UnityActivity" android:value="true" /> | |
<intent-filter> | |
<action android:name="android.intent.action.VIEW" /> | |
<category android:name="android.intent.category.DEFAULT" /> | |
<category android:name="android.intent.category.BROWSABLE" /> | |
<data android:scheme="googleAuth" /> | |
</intent-filter> | |
</activity> | |
<!-- Options for in-app testing on your global store --> | |
<!-- <meta-data android:name="onestore:dev_option" android:value="global" /> --> | |
<service android:name="com.google.firebase.messaging.MessageForwardingService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"></service> | |
<meta-data android:name="com.oneadmax.global.appkey" android:value="~~~~" /> | |
<meta-data android:name="applovin.sdk.key" android:value="~~~~" /> | |
</application> | |
</manifest> | |
------- c# 결제나 광고 같은 중요한 요소 멀티스레드에서 중복해서 실행되면 안되는 것들은 lock 을 사용해서 안전하게 실행하자 ------ | |
lock 안의 내용을 중복 실행하지 않는다. | |
private readonly object _adLock = new object(); | |
이런식으로 선언 후 | |
public void LoadOneAdReward() | |
{ | |
if (_oneAdRewardVideoAd == null) | |
{ | |
Debug.LogError("The Reward video ad is null."); | |
return; | |
} | |
lock(_adLock) { | |
_oneAdRewardVideoAd.Load(); | |
} | |
} | |
잠그고 싶은 곳에 lock(락용 오브젝트) { 안전코드 } 이런식으로 처리하면 간단하게 구현할 수 있다. | |
* try...finally 예외처리를 사용할 경우 예외가 throw 되더라도 잠금이 해제 | |
* await 연산자 사용 불가 | |
------- 오디오 압축은 22%각 국룰 Default로 해버려서 Android랑 IOS 한번에 처리하자 ------- | |
------- 업로드키 / 앱서명키 확실히 집고 넘어가기 ------ | |
구글 플레이 콘솔 기준으로 설정 > 앱 서명에 aab 업로드하면 앱서명키와 업로드키가 나오는데 | |
쉽게 생각해서 | |
1. unity build 할때 keystore 만든 키 == 업로드키 | |
(SHA-256 커맨트에서 keytool -list -keystore app.keystore 로 key체크해보면 SHA-256 똑같을거임) | |
2. 각 스토어에서 제공하는 인증서 (One Store, App Store, Google Play 각각에서 받아야함) == 앱서명키 | |
------- 너무 빠른 시간내에 runtime에서 image의 sprite를 교체할 경우 코드가 씹히는 경우가 발생 ------- | |
(2024.03 2022.3.10f1) | |
------- 외부 플러그인 Resources 폴더 Build 시에 제외해야할 경우 ------- | |
가장 좋은건 플러그인 불러올때 Example 폴더 불러오지 않는거고 | |
1. 꼭 필요한거면 Editor 폴더 안에 Resources폴더를 넣는게 가장 좋은 방안. | |
에디터에서만 보면 되니까 | |
2. ~Resources 이런식으로 ~붙여서 빼버리는 방법도 있음. | |
3. 이런식으로 빌드 시에만 제외시키는 방안도 있음. | |
using UnityEditor; | |
using UnityEditor.Build; | |
using UnityEditor.Build.Reporting; | |
using System.IO; | |
public class ExcludeResourcesPreprocessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport | |
{ | |
public int callbackOrder { get { return 0; } } | |
private string originalPath = "Assets/Plugins/ExternalPlugin/Resources"; | |
private string tempPath = "Assets/Plugins/ExternalPlugin/_Resources"; | |
public void OnPreprocessBuild(BuildReport report) | |
{ | |
// 빌드 전에 Resources 폴더 이름 변경 | |
if (Directory.Exists(originalPath)) | |
{ | |
Directory.Move(originalPath, tempPath); | |
AssetDatabase.Refresh(); | |
} | |
} | |
public void OnPostprocessBuild(BuildReport report) | |
{ | |
// 빌드 후에 폴더 이름을 원래대로 복구 | |
if (Directory.Exists(tempPath)) | |
{ | |
Directory.Move(tempPath, originalPath); | |
AssetDatabase.Refresh(); | |
} | |
} | |
} | |
-------- TMP_Font 최적화 | |
1. TMP_Font 쉐이더 수정한 파일을 여러개 만들면 그만큼 폰트 용량이 늘어나기 때문에 Preset Material만 만들어서 TMP Component의 Material Preset 드롭박스에서 골라주면 된다. | |
코드로 바꿀때도 그냥 머터리얼만 바꿔주면 됨. | |
만드는 방법은 Outline이나 Glow 효과 쉐이더 오른쪽 땡땡이 눌러서 Create Material Preset 누르면 끝. | |
(생성된 메터리얼 이름이나 위치 아무렇게나 놔도 Metarial Preset 드롭박스에서 체크할 수 있다. | |
추가적으로 더 최적화 하고싶으면 Shader를 TextMeshPro/Distance Field가 아닌 TextMeshPro/Mobile/Distance Field 로 변경해라) | |
-------- timeline - PlayableDirector 정리 --------- | |
(2024.03. 2022.3.10f1 기준) | |
WrapMode - None 이면 애니메이션이 끝났을때 Stop되고 아닐경우(그 외 Loop/Hold) Play가 유지됨. | |
어떤 상황에서든 Stop 되면 time 값이 0이 되버림. | |
Stop 시 Animator의 Apply Root Motion 처럼 리셋하는 기능이 없기 때문에 코드로 제어가 필요함. | |
(0프레임에 초기화 프레임으로 잡고 (0프레임은 초기화 하는 영역이고 실행 안시킨다고 생각하고 작업하면 됨.) | |
코드로 Stop시 0프레임으로 Evaluate 실행하는식으로 | |
Play는 1프레임부터 시작하도록 설정 | |
Stop시 Evaluate 코드가 안먹는걸로 판단되서 Stop되기 직전에 Evaluate 시키는 식으로 처리했음) | |
따라서 Play, Stop, Pause 쓰지말고 커스텀 코드를 만들어서 실행시키는 걸 추천 | |
(주의* 이렇게 사용시에 AnimationClip의 Loop 를 반드시 꺼야한다. 어짜피 루프는 Timeline에서 제어하므로) | |
TODO - 저 코드도 StopTL 안쓰고 강제로 Stop 되버릴 시 그 프레임에서 정지 될 여지가 있으므로 그 부분 처리까지 고려한 설계가 필요함. | |
예시) | |
IEnumerator LoopTimelinePlay(bool isLoop = true, Action onComplete = null) { | |
do { | |
pD_Tutorial.initialTime = 1.2d/60d; | |
pD_Tutorial.time = 1.2d/60d; | |
pD_Tutorial.Play(); | |
yield return new WaitUntil(() => pD_Tutorial.time >= pD_Tutorial.duration*0.99f || pD_Tutorial.state != PlayState.Playing); | |
} | |
while(isLoop); | |
onComplete?.Invoke(); | |
pD_Tutorial.time = 0.0d; | |
pD_Tutorial.Evaluate(); | |
pD_Tutorial.Stop(); | |
isNextStep = true; //1회성 애니메이션 기다리는 플래그 변수 | |
} | |
void PlayTL(bool isLoop_ = true, Action onComplete_ = null) { | |
if(_runAnimator!=null) { | |
StopCoroutine(_runAnimator); | |
} | |
StartCoroutine(_runAnimator = LoopTimelinePlay(isLoop_, onComplete_)); | |
} | |
void StopTL() { | |
if(_runAnimator!=null) { | |
StopCoroutine(_runAnimator); | |
} | |
pD_Tutorial.time = 0.0d; | |
pD_Tutorial.Evaluate(); | |
pD_Tutorial.Stop(); | |
} | |
-------- 코루틴 WaitUntil도 매우 유용함 특정 조건까지 기다림 ------- | |
IEnumerator RepeatTimelineFromTimeCoroutine() | |
{ | |
while (true) | |
{ | |
director.Play(); | |
// 지정된 종료 시간까지 기다림 | |
yield return new WaitUntil(() => director.time >= repeatEndTime); | |
// 지정된 시작 시간으로 돌아감 | |
director.time = repeatStartTime; | |
director.Play(); | |
} | |
} | |
-------[ContextMenu("Do Something")] ------- | |
인스펙터 오른쪽 아이콘에 버튼이 생기고 누르면 작동하게 할 수 있는 Attribute | |
값 자동으로 입력시킬때나 Reset용도로 많이 사용한다. | |
------ 애니메이션 동적으로 값 할당하는 방법 SetCureve 로 Runtime에서 애니메이션 바꾸는거 clip을 Legacy로 바꾸는 코드 적용해야 가능한데 | |
그러면 animator나 timeline을 못쓰니까 동적으로 바꾸는건 불가능하다. | |
결론 값 수정은 불가능, 클립을 동적으로 생성해서 Animation에 붙이는것만 가능하다. | |
(사실상 animator 설계가 구리기 때문에 코드로 제어하기 쉬운 Timeline으로 처리) ------- | |
* 값을 바꾸는건 Android 에서 불가능한걸 체크했음. 결국 동적인부분은 DoTween Sequence 로 처리하고 정적인 부분은 Timeline으로 처리했음. | |
------ Android mainTemplate.gradle 에서 dependencies 임시로 제거하려면 안쓰는 모듈 때어내려면 ------- | |
각 모듈의 Editor 폴더 가서 ~~~Dependencies.xml 에서 <dependencies> 태그만 제외하고 안에 내용 모두 주석처리하면됨 | |
------ 두점을 가지고 회전해서 들어가는 효과를 표현하려면 Vector3.Slerp 사용하면 됨. | |
------ 돈이나 점수나 이런걸 파티클이나 애니메이션이 처리되고 나서 | |
예를들어 동전이 UI에 들어오고 나서 값을 바꾸고 싶은 경우 많은데 이런식으로 구현하면 됨. ----- | |
public UnityEvent<int> CreateTurnParticle = new UnityEvent<int>(); //생성(수집 할) 개수 | |
public UnityEvent<int> CollectTurnParticle = new UnityEvent<int>(); //수집 개수 | |
1. 값을 먼저 바꾼다. | |
2. int나 값 매개변수 이벤트로 파티클 요소의 수나 올릴값을 측정한다. 생성이벤트에선 변수에 합하고 도달시 콜백이벤트에선 뺀다. | |
3. 값을 변해주는 애니메이션에서 그 합한변수 이용해서 처리 | |
ex) GetCurrentBoard().boardUI.tC_LeftTurn.ChangeTextAnim(Mathf.Clamp(LeftTurn-collectingCount, 0, 9999), 0); | |
------ 유니티 애니메이터 버그 ------ | |
(2022.3.10f1 기준) Animator SetInteger 함수 적용이 안 되길래 0.1f초 뒤에 코드를 실행하니까 적용됨. | |
IEnumerator TutorialRun() { | |
Init(); | |
_GC.game.GetCurrentBoard().blocks.Add(new Block(0,3)); | |
_GC.game.GetCurrentBoard().blocks.Add(new Block(1,3)); | |
_GC.game.GetCurrentBoard().blocks.Add(new Block(2,3)); | |
_GC.game.GetCurrentBoard().blocks.Add(new Block(0,5)); | |
_GC.game.GetCurrentBoard().blocks.Add(new Block(1,5)); | |
_GC.game.GetCurrentBoard().blocks.Add(new Block(2,5)); | |
// a_Tutorial.SetTrigger("TutorialAnimStart"); | |
yield return new WaitForSeconds(0.1f); | |
tutorialStep = TutorialStep.SQUARE_ANIM; | |
a_Tutorial.SetInteger("currentStep", (int) tutorialStep); | |
} | |
------ ObjectPool 사용 시 Reset시에 DOTween Reset을 꼭 해야 버그 없음 ------- | |
obj_.GetComponent<RectTransform>().DOKill(); | |
obj_.transform.SetParent(t_DifficultyIconPool); | |
이런식으로 rectTransform이나 transfrom DOKill해주는게 좋음. | |
------ rectTransform의 가운데 점 center의 world값 가져오는 코드 ------- | |
this.rectTransform.TransformPoint(rectTransform.rect.center) | |
------ rendertexture.create failed depth/stencil format unsupported - d32 sfloat s8 uint (94) 에러 ------ | |
안드로이드에서 RenderTexture 사용 시 Depth Stencil Format 을 D32 S8 말고 D24 S8을 사용해야함. (2022.3.10f1 2024.02 기준) | |
------ EasySave 클래스 단위로 저장할때 가끔 클래스 수정하고 나서 데이터 클래스를 Default로 지운 다음 다시만들어야 됨. ----- | |
------ Firebase Realtime DB 싱가포르일 경우 세팅 -------- | |
public static void InitDB() { | |
AppOptions options = new AppOptions{ DatabaseUrl = new Uri("https://fourtris-406508-default-rtdb.asia-southeast1.firebasedatabase.app/")}; | |
App = FirebaseApp.Create(options); | |
DBRef = FirebaseDatabase.DefaultInstance.RootReference; | |
} | |
----- Firebase Realtime DB 규칙 예제 ------ | |
"rules": { | |
"users": { | |
"$uid": { | |
".write": "$uid === auth.uid", | |
".read": true, | |
}, | |
"TEST_USER": { | |
".write": true, | |
".read": true | |
} | |
} | |
} | |
----- Firebase Auth 에서 Oauth 설정하려면 보통 Google Cloud Oauth 웹 어플리케이션으로 처리해서 등록하면 됨 ------ | |
----- DOTween To OnUpdate에서 돌리면 정확한 값까지 도달 안 하는경우가 생기는데 마지막 OnComplete에서 해당 값으로 강제할당 시키는 방식으로 마무리 해야함 ----- | |
----- DOTween Sequence 사용 시 주의사항 -------- | |
_seq = DOTween.Sequence(); | |
이러면 오로지 하나의 시퀀스만 돌아갈 수 있음. | |
_seq = DOTween.Sequence(gameObject); | |
이렇게 사용해야 덮어씌워지지 않음. | |
하나의 Tween만 사용되기에 다른 오브젝트에서 동시에 Sequence 사용시 이런식으로 this나 gameObject를 붙여서 그 오브젝트 안에서 돌게 해야함. | |
----- Unity vscode 인텔리전스(참조) 안 될때 해결방법 (2022.3.1f11 2024.02 기준)------ | |
Window > Package Manager > Visual Studio Editor 2.0.21 -> 되어있는거 2.0.22 최신버전으로 업데이트 | |
(vscode는 당연히 업뎃이고 그거 말고 vs editor를 업데이트를 해야함!) | |
Edit > Perferences > External Tools > Visual Studio Code 본인같은 경우 [1.86.0-> 1.86.2] 최신으로 되어 있는지 체크하고 | |
Vscode만 다시 껐다 키면 해결됨! | |
----- 유니티에서 리얼 카메라 뷰 ugui RawImage에 띄우는 코드 (2024.02.16 기준) ----- | |
public class WebCamUI : MonoBehaviour | |
{ | |
[SerializeField] private RawImage rI_ChangedImage; | |
[SerializeField] private Texture s_NormalBG; | |
private WebCamTexture _webCam; | |
public void AROnOff(bool isOn) { | |
if(isOn) { | |
#if UNITY_ANDROID | |
//카메라 퍼미션이 허용 됬을때만 | |
if(Permission.HasUserAuthorizedPermission(Permission.Camera)) { | |
AROn(); | |
} else { | |
PermissionCallbacks permissionCallbacks = new(); | |
permissionCallbacks.PermissionGranted += AROn; | |
permissionCallbacks.PermissionDenied -= (pName_) => { | |
//퍼미션 거부 | |
Debug.Log("No permission cameras"); | |
AROnOff(false); | |
}; | |
Permission.RequestUserPermission(Permission.Camera, permissionCallbacks); | |
} | |
#endif | |
} else { | |
#if UNITY_ANDROID | |
if(_webCam) { | |
_webCam.Stop(); | |
} | |
#endif | |
rI_ChangedImage.texture = s_NormalBG; | |
} | |
} | |
private void AROn(string permissionName_ = null) { | |
//이미 있을경우 삭제 메모리 관리 | |
if(_webCam) { | |
Destroy(_webCam); | |
_webCam=null; | |
} else {} | |
if(WebCamTexture.devices.Length==0) { | |
// 디바이스가 없는 경우 | |
Debug.Log("No devices cameras found"); | |
AROnOff(false); | |
return; | |
} else { } | |
var _backCams = WebCamTexture.devices.Where(d => !d.isFrontFacing).Select(d => d).ToList(); | |
if(_backCams.Count() > 0) { | |
var requestCam = _backCams[0]; | |
int requestWidth = Screen.width; | |
int requestHeight = Screen.height; | |
//원하는 비율로 표현해야할 경우 | |
bool isAbleRatio = false; | |
foreach(var cam in _backCams) { | |
if(isAbleRatio) { break; } | |
foreach(var res in cam.availableResolutions) { | |
if(!isAbleRatio&&GetAspectRatio(res.width, res.height).Equals(GetAspectRatio(Screen.width, Screen.height))) { | |
requestWidth = res.width; | |
requestHeight = res.height; | |
requestCam = cam; | |
isAbleRatio = true; | |
break; | |
} | |
} | |
} | |
_webCam = new WebCamTexture(requestCam.name, requestWidth, requestHeight) | |
{ | |
filterMode = FilterMode.Trilinear, | |
requestedFPS = 60 | |
}; | |
_webCam.Play(); | |
rI_ChangedImage.texture = _webCam; | |
} else { | |
//후방카메라 없는경우 | |
Debug.Log("No back camera"); | |
AROnOff(false); | |
return; | |
} | |
} | |
private string GetAspectRatio(int width, int height, bool allowPortrait = true) { | |
if (!allowPortrait && width < height) Swap(ref width, ref height); //세로가 허용되지 않는데, (가로 < 세로)이면 변수값 교환 | |
float r = (float)width / height; | |
return r.ToString("F2"); | |
} | |
private void Swap<T>(ref T a, ref T b) { | |
T tmp = a; | |
a = b; | |
b = tmp; | |
} | |
private void Update() { | |
UpdateWebCamRawImage(); | |
} | |
private void UpdateWebCamRawImage() { | |
if(!_webCam) return; | |
int _videoRotAngle = _webCam.videoRotationAngle; | |
rI_ChangedImage.transform.localEulerAngles = new Vector3(0, 0, -_videoRotAngle); //카메라 회전 각도 반영 | |
int _width, _height; | |
if(Screen.orientation == ScreenOrientation.Portrait || Screen.orientation == ScreenOrientation.PortraitUpsideDown) { | |
_width = Screen.width; | |
_height = Screen.width * _webCam.width / _webCam.height; // 세로일경우, 가로 고정 후 _webCam의 비율에 따라 세로를 조절 | |
} else { | |
_height = Screen.height; | |
_width = Screen.height * _webCam.width / _webCam.height; | |
} | |
if(Mathf.Abs(_videoRotAngle) % 180 != 0f) { Swap(ref _width, ref _height); } //_webCam 자체가 회전되어있는 경우 가로/세로 값을 교환 | |
rI_ChangedImage.rectTransform.sizeDelta = new Vector2(_width, _height); //이미지의 size로 지정 | |
} | |
} | |
----- 애니메이터 StateMachineBehavior state 마지막 프레임 끝났을때 처리하는 스크립트 ----- | |
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) | |
{ | |
GameController.Inst.prologueUI.b_nextPrologue.interactable = false; | |
Debug.Log($"START ANIM : {stateInfo.shortNameHash}"); | |
} | |
// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks | |
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) | |
{ | |
if(!_isFinished&&stateInfo.normalizedTime >= 0.99f) { | |
_isFinished = true; | |
GameController.Inst.prologueUI.b_nextPrologue.interactable = true; | |
Debug.Log($"END ANIM : {stateInfo.shortNameHash}"); | |
} | |
} | |
normalizedTime 1.0f으로 하지말고 0.99이상으로 설정해놓기 | |
----- 애니메이터 Transition에서 Has Exit Time 체크 꺼놓는거랑 상관없이 아래 전환구간 설정하는거 완전히 없애야 다음 번 전환시 문제 안생김 ----- | |
----- 애니메이터 스테이트들 모두 초기화 animator reset 시키는 방법 ------ | |
animator.Rebind(); | |
animator.Update(0); | |
----- 리플렉션 Set하는 예제 ------ | |
typeof(PoundingStep).GetField($"isPress{index_+1}Button").SetValue(this, false); | |
PoundingStep 클래스의 isPress1Button 불 변수의 값을 false로 Set하는 함수 다만 변수가 public 이여야함. | |
------ 앱 버전 / bundleVersion 연동 간단코드 | |
[CustomEditor (typeof (AppVersion))] | |
public class AppVersionEditor : Editor | |
{ | |
AppVersion instance; | |
public void OnEnable () | |
{ | |
instance = (AppVersion)target; | |
PlayerSettings.bundleVersion = instance; | |
} | |
public override void OnInspectorGUI () | |
{ | |
base.OnInspectorGUI (); | |
if (GUI.changed) | |
{ | |
PlayerSettings.bundleVersion = instance; | |
} | |
} | |
} | |
------ GUI 간단 테스트 UI 만들기 텍스트 색까지 간단하게 ---- | |
if(GUILayout.Button($"DATA\nRESET", new GUIStyle(GUI.skin.button) { fontSize = 40, normal = new GUIStyleState() { textColor = Color.red } }, GUILayout.Height(180), GUILayout.Width(180))) { | |
DataManager.ResetLocalData(); | |
} | |
------ 깊은복사 레퍼런스타입 아닐때 Dictionary 쉽게 하는방법 ------ | |
Dictionary<string, int> dictionary = new Dictionary<string, int>(); | |
Dictionary<string, int> copy = new Dictionary<string, int>(dictionary); | |
Dictionary<string, string> dict2 = new Dictionary<string, string>(dict); | |
------ 다시 한번 적는 for 반복문 안에서 익명함수(lambda 식)으로 i늘리면 마지막 값으로 세팅되는 경우 해결방법 ----- | |
for(int i=0; i<b_decoBuys.Count; i++) { | |
int iValue = i; | |
b_decoBuys[i].onClick.AddListener(() => { | |
CheckAndBuyDeco(iValue); | |
}); | |
} | |
for문 안에 int iValue = i 선언 | |
lambda 안에만 iValue 넣으면 됨. | |
------ flag enum 비트연산자로 특정 enum만 지우고 싶을때 ------ | |
(DecoType) (((int) currentDeco) & ~(1<<_index)) | |
비트연산자 NOT으로 특정이넘만 0으로 만들고 &연산자로 그 이넘만 0으로 세팅 하면 됨. | |
------ 2D 에서 Y축 기준으로 정렬하는 방법 ----- | |
Transparency Sort Mode | |
Edit > Project Settings > Graphics > Custom Axis 하고 Y를 1로 | |
------ Git 사용할때 Github에 이미 올린 메타파일이 Github에 남아있는 경우 git rm --cached -r meta파일 명령어로 지워주고 반드시 커맨드로 commit 후 push해 지워줘야 함. | |
------- 변수명이 num1 num2 ... 이런 상황이고 num{?} 숫자로 DataManager의 변수들을 코드로 가져오고싶을 때 | |
GetField() 리플렉션 사용하면 됨. | |
ex) DataManager에 public static readonly int SURVIVAL_CLEAR_COUNT_1_ADDTURN = 1; 이런 변수를 가져오고싶을 때 | |
(int) typeof(DataManager).GetField($"SURVIVAL_CLEAR_COUNT_{count_}_ADDTURN").GetValue(this); // 1 | |
------ Missing Default Layout ------ | |
No vaild user created or default window layout found. | |
에러나면 | |
Library > ArtifactDB 랑 ArtifactDB-lock 만 지우고 다시 키면 좀 걸리긴하지만 해결됨. | |
----- Linq ToList()로 무조건 깊은복사가 일어나는게 아님!! ---- | |
Linq ToList() 깊은복사는 값 타입일경우만 레퍼런스 타입일 경우 깊은복사 안일어남 | |
ConvertAll(new 해서 생성하거나) Select 한 후(new 생성) .ToList() 하는 방식을 사용해야 깊은복사 가능 | |
아니면 Copy 함수구현해두거나 | |
List<Book> books_2 = books_1.Select(book => new Book(book.title)).ToList(); | |
List<Book> books_2 = books_1.ConvertAll(book => new Book(book.title)); | |
아니면 엄청 간단한 방법 람다 사용 | |
var books2 = books1.Select(b => b with { }).ToList(); | |
----- 인터넷 연결 체크 ----- | |
유니티에서 연결 안됨, 데이터 연결, 와이파이 연결 체크 가능 | |
NetworkReachability enum에 와이파이랑 데이터 있음. | |
ex) | |
public static bool IsNetworkConnected() { | |
if(Application.internetReachability == NetworkReachability.NotReachable) { | |
return false; | |
} else { | |
return true; | |
} | |
} | |
----- Firebase는 Task로 리턴되는 경우 많기에 비동기 처리로 데이터를 처리해야한다. await 와 async 사용이 필수 | |
보통 처리 성공 실패 Task<bool> 로 리턴해서 Task<bool> 변수.Result 로 bool 값 체크하면 됨. | |
ex) | |
public static async Task<bool> TrySaveData() { | |
bool _isSaveSuccess = false; | |
if(DBRef == null) { | |
InitDB(); | |
} | |
// var str = new ES3File(ES3Settings.defaultSettings).LoadRawString(); //.Substring(1); | |
// str.Substring(0, str.Length - 1); | |
if(DataManager.userData == null) { | |
DataManager.userData = new UserData(); | |
} | |
var str = JsonConvert.SerializeObject(DataManager.userData, Formatting.Indented); | |
// str = str.Replace(": {", ": ["); | |
// str = str.Replace("},", "],"); | |
Debug.Log(str); | |
// if(Auth != FirebaseAuth.DefaultInstance) { | |
await DBRef.Child("users").Child(GetFirebaseUID()).SetRawJsonValueAsync(str).ContinueWithOnMainThread(task => { | |
if (task.IsFaulted) { | |
Debug.LogError($"DB SAVE FAIL !!\n{task}"); | |
_isSaveSuccess=false; | |
} | |
else if (task.IsCompleted) { | |
Debug.Log($"DB SAVE DONE\n{str}"); | |
_isSaveSuccess=true; | |
} | |
}); | |
// } | |
return _isSaveSuccess; | |
} | |
------- GPGS Helper 코드 (2024.01) ------- | |
public static void LoginGooglePlayGames() { | |
PlayGamesPlatform.Instance.Authenticate((success) => | |
{ | |
if (success == SignInStatus.Success) | |
{ | |
Debug.Log("Login with Google Play games successful."); | |
PlayGamesPlatform.Instance.RequestServerSideAccess(true, code => | |
{ | |
Debug.Log("Authorization code: " + code); | |
FirebaseHelper.AuthFirebaseUsingGPGS(code); | |
// This token serves as an example to be used for SignInWithGooglePlayGames | |
}); | |
DataManager.userData.userId = GetLoginToken(); | |
} | |
else | |
{ | |
#if !UNITY_EDITOR | |
Debug.Log("Login Unsuccessful"); | |
DataManager.Inst.isCloudChecking = false; | |
#endif | |
} | |
}); | |
} | |
public static string GetLoginToken() { | |
if(!PlayGamesPlatform.Instance.IsAuthenticated()) { | |
LoginGooglePlayGames(); | |
} | |
string _IDtoken = ((PlayGamesLocalUser)Social.localUser).id; | |
if(string.IsNullOrEmpty(_IDtoken)) _IDtoken = "none"; | |
// Debug.Log(_IDtoken); | |
return _IDtoken; | |
} | |
------ 65K 문제 발생시 ----- | |
gradleTemplate.properties 에 android.multiDexEnabled=true 추가 | |
mainTemplate.gradle, launcherTemplate.gradle 두개 다 그래들에 추가 | |
android { | |
defaultConfig { | |
multiDexEnabled true | |
} | |
} | |
------ AppLovin Plugins > Android > kotlinx_coroutines_core 문제로 빌드 안될때 | |
Custom Main Gradle Template 체크해서 | |
// Android Resolver Exclusions End | |
android { | |
packagingOptions { | |
exclude('META-INF/kotlinx_coroutines_core.version') //여기 추가 | |
} | |
ndkPath "**NDKPATH**" | |
compileSdkVersion **APIVERSION** | |
buildToolsVersion '**BUILDTOOLS**' | |
compileOptions { | |
sourceCompatibility JavaVersion.VERSION_11 | |
targetCompatibility JavaVersion.VERSION_11 | |
} | |
Custom Launcher Gradle Template 체크해서 | |
launcherTemplate.gradle 만들고 | |
}**PACKAGING_OPTIONS****PLAY_ASSET_PACKS****SPLITS** | |
**BUILT_APK_LOCATION** | |
packagingOptions { | |
exclude('META-INF/kotlinx_coroutines_core.version') // 빌드 시 이거 추가해서 kotlinx 제외 시키면 됨. | |
} | |
Custom Gradle Properties Template 체크 | |
Custom Gradle Settings Template 체크 도 선행되어야한다. | |
------ Simulator 랑 Game 뷰 중에서 Game뷰로 테스트해라 Simulator에서 종종 인풋 뻗음 그냥 해상도 체크할때나 쓰는게 Simulator 임 ----- | |
------ 안드로이드 Android iOS 앱 출시 전 세팅 (2024.01 기준) ------- | |
Compression Method - LZ4 는 게임내 속도가 중요시에 설정 10%정도 차이남, | |
LZ4HC 로 하면 파일 압축률, 로딩 속도면에서 좋음. | |
작은 규모의 속도가 중요하지 않는 게임일 경우 LZ4HC 아닐 경우 LZ4 | |
Color Space* - Linear 색은 감마는 구시대 쓰는거 아님. 리니어가 디폴트고 성능차이 생각할 필요도 없다. | |
Override Default Package Name 체크 ex) com.직접입력.하기 | |
Minimum API Level - Android 6.0 (크게 중요하지않음 2024.01 현재는 6.0 정도 추천) | |
Target API Level - Android 13.0 (현재 해당버전 이슈가 없다면 가장 최신 추천) | |
Scripting Backend > IL2CPP 로 바꾸기 | |
Api Compatibility Level* 가장 최신꺼로 ex) .Net Standard 2.1 | |
Target Architectures | |
ARMv7 체크 | |
ARM64 체크 | |
Project Keystore 만들기 | |
Build > Custom Main Manifest 체크, Custom Proguard File 체크 | |
Custom Proguard File에는 난독화 안할 항목 넣어야 해서 | |
생성된 proguard-user.txt 파일에 | |
아래 내용 넣기 | |
-keep class com.google.android.gms.**{ *; } | |
-keep class com.google.firebase.**{ *; } | |
-keep class com.google.games.**{ *; } | |
-keep interface com.google.games._* { *; } | |
Minify > Release Check 하기 (난독화 사용) | |
------ 플레이스토어 출시 할때 경고 ------ | |
이 App Bundle 유형과 연결된 가독화 파일이 없습니다. | |
난독화된 코드(R8/proguard)를 사용하는 경우 가독화 파일을 업로드하면 비정상 종료 및 ANR을 더 쉽게 분석하고 디버그할 수 있습니다. | |
R8/proguard를 사용하면 앱 크기를 줄이는 데 도움이 됩니다. | |
노란 경고 뜨는데, 기본값은 Proguard 이고 Use R8, Release 체크박스를 선택해야함 | |
File > Build Settings > Player Settings > Player > Publishing Settings > Minify > Use R8, Release 체크 | |
------ 구글 플레이스토어 등록할 때 Android AD_ID 설정 꼭 해야함. ------ | |
Plugins > Android > GoogleMobileAdsPlugin.androidlib > AndroidManifest.xml 에 | |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
package="com.google.unity.ads" | |
android:versionName="1.0" | |
android:versionCode="1"> | |
<application> | |
<uses-library android:required="false" android:name="org.apache.http.legacy"/> | |
</application> | |
<uses-permission android:name="com.google.android.gms.permission.AD_ID" /> | |
</manifest> | |
<uses-permission android:name="com.google.android.gms.permission.AD_ID" /> 이거 한줄 추가해줘야함 <application> 밖에 넣어줘야함! | |
광고 ID 권한 | |
------ Shader 로 씬에서 실행중 아닐때도 계속 효과 확인할려면 상단에 레이어에 반짝이는 아이콘 클릭 후 Always Refresh 체크하면 됨. ------ | |
------ Action onComplete 같이 델리게이트 등록할 때 여러번 등록될 수 있을경우 한번 실행하고 빼야할 때, 받은 함수를 빼는 함수를 뒤에 추가한다음 추가한 함수를 등록하면 됨. ----- | |
ex) | |
if(onComplete != null) { | |
onComplete += () => { | |
_interstitialAd.OnAdFullScreenContentClosed -= onComplete; | |
}; | |
_interstitialAd.OnAdFullScreenContentClosed += onComplete; | |
} | |
------ TimeStamp 두개 값 비교했을 때 같음. 따라서 Stamp변수에 담고 비교연산이나 초단위 추가 연산해서 처리하기도 편함 ------ | |
long result1 = ((DateTimeOffset)DateTime.UtcNow.AddHours(9).AddSeconds(60)).ToUnixTimeSeconds(); | |
long result2 = ((DateTimeOffset)DateTime.UtcNow.AddHours(9)).ToUnixTimeSeconds() + 60; | |
Debug.Log($"{result1} : {result2}"); | |
------ ?. ?? 로 null 검사 처리 하는 습관 매우 유용하다. default 값 처리할때도 좋음. ------- | |
------ Simulator로 디바이스 별 테스트 할 수 있다. ---------- | |
------ 스마트폰 가장자리 노치디자인 notch 지원을 위한 Safe Area 코드로 안드로이드나 아이폰 해상도 짤리는 부분을 커버하는 코드를 적용해야한다. ------ | |
적용할 Canvas 아래에 Contents 나 이런식으로 Stretch / Stretch (꽉찬 설정) 으로 한 RectTransform 만들고 그 아래에 내용을 넣는다. | |
그 오브젝트에 코드를 적용해야됨 | |
ex) | |
using UnityEngine; | |
public class UISafeAreaManager : MonoBehaviour | |
{ | |
public static void ApplySafeAreaPosition(RectTransform rt) | |
{ | |
Rect safeArea = Screen.safeArea; | |
// Convert safe area rectangle from absolute pixels to normalised anchor coordinates | |
Vector2 anchorMin = safeArea.position; | |
Vector2 anchorMax = safeArea.position + safeArea.size; | |
// 기존 anchor x 좌표 사용 | |
anchorMin.x = rt.anchorMin.x; | |
anchorMax.x = rt.anchorMax.x; | |
anchorMin.y /= Screen.height; | |
anchorMax.y /= Screen.height; | |
rt.anchorMin = anchorMin; | |
rt.anchorMax = anchorMax; | |
} | |
} | |
------ ***** 개꿀 정보 Ctrl + K 누르면 유니티 내에서 빠른 검색 가능 ------ | |
에디터 내에 프로젝트 파일이나 하이라키나 에셋스토어 검색까지 가능 미친 기능 모르면 인생손해... | |
------ unity new inputsystem 에서 android back 버튼 누를 시 처리 ----- | |
Action Map에서 Back [Any] 해서 기기 Mobile 체크하면 됨. | |
private void Start() { | |
input.Touch.TouchPress.started += ctx => OnTouchDown(ctx); | |
input.Touch.TouchPress.canceled += ctx => OnTouchUp(ctx); | |
input.Touch.TouchEscape.performed += ctx => OnEscapePressed(ctx); | |
GC = GameController.Inst; | |
} | |
private void OnEscapePressed(InputAction.CallbackContext context) { | |
Debug.Log("Back Button Pressed!"); | |
if(GC.lobbyUI.gameObject.activeSelf) { | |
#if UNITY_EDITOR | |
UnityEditor.EditorApplication.isPlaying = false; | |
#elif UNITY_ANDROID && !UNITY_EDITOR | |
if(Application.platform == RuntimePlatform.Android) { | |
Application.Quit(); | |
} | |
#endif | |
} else if(GC.gameUI.gameObject.activeSelf) { | |
if(GC.gameUI.GetFSM.State == StageGameStatus.IDLE) { | |
GC.gameUI.GetFSM.ChangeState(StageGameStatus.LOAD); | |
DataManager.SaveData(); | |
GC.OnGoToLobby(); | |
} | |
} | |
} | |
------ android면 in-app review api 사용하는게 좋고, ios는 내장 함수가 있다. | |
------ firebase Dictionary 타입 저장할때 Dictionary<string, object> 형태로 저장해야하는데 혹시라도 | |
a["0"] = 1, a["1"] = 1 이런식으로 key를 숫자로만 할 경우 저장에는 문제 없으나 firebase에서 로드할때 List로 불러오는 참사가 일어남 ------- | |
----- #UNITY_ANDROID 라고하면 안드로이드일때만 실행되는게 아니라 안드로이드 또는 안드로이드 에디터일때도 실행됨 ----- | |
----- EasySave3 json변환방식 구리다. Firebase Realtime DB랑 호환 안됨. ------ | |
----- JsonUtility 쓰지말자 아직도 Dictionary 지원안함 뉴턴 .net json이 진리임 ------- | |
----- CLOUD 저장/로드에 대한 고찰 ---- | |
GPGS 클라우드 저장로드 상당히 안좋음. | |
Firebase 쓰는게 정답, 에디터 상에서 테스트도 가능. | |
Firebase - GPGS 연동할때 Google Cloud 에서 API 설정에서 OAuth 키 Android 용으로 하나 만들고 콘솔 keytool로 SHA-1 인증서 지문 가지고 (Unity keystore 정보 가지고 ) 그걸 파이어베이스 설정에서 사용하기 | |
Auth에선 GPGS로 로그인하기 키고, 구글 클라우드 웹 API 계정과 비번 사용하면 됨. | |
Firebase Realtime DB랑 FireStore DB 둘중에 하나 선택해서 하면 됨. | |
Firebase 설정을 수정한 후 google-service.json 파일을 다시 받아서 다시 프로젝트에 넣어야함. | |
------ iOS Resolver / iOS Cocoapods 설치 문제 발생시 최신 XCode 다운 받은 후 기존 XCode 커맨드 재설정 필요 -------- | |
----- GPGS 로그인 토큰 받기 ------- | |
//유저 토큰 받기 첫번째 방법 | |
//string _IDtoken = PlayGamesPlatform.Instance.GetIdToken(); | |
//두번째 방법 | |
string _IDtoken = ((PlayGamesLocalUser)Social.localUser).GetIdToken(); | |
//인증코드 받기 | |
string _authCode = PlayGamesPlatform.Instance.GetServerAuthCode(); | |
LogGPGS("authcode : " + _authCode + " / " + "idtoken : " + _IDtoken); | |
GPGS에서 PlayGamesClientConfiguration 코드는 0.11 버전에서 Deprecated 됨. 따라서 알아서 설정하기에 굳이 적을필요없다. | |
------ GameGuardian 게임가디언으로 해킹 테스트해보기 ----- | |
변수 변조 막는방법으로 Anticheat 뚫고 뚤릴때 있으니까 | |
메모리 변조 프로퍼티 사용해서 get set 할때 해쉬값 비교 해쉬값 같이 바꿔주고 되돌리는건 기존 데이터 저장해서 바꾸기. | |
------ Hackle은 이벤트기반으로 가설을 세우는게 중요하고 출시하기 직전에 붙이면 됨 ------- | |
붙이고 리텐션올리는 형태로 테스트 많이하는편 | |
------- UNITY_EDITOR, UNITY_ANDROID 전처리기 말고 ------ | |
전처리 조건문 잘 사용하면 좋음. | |
Edit → Project Settings → Player → Script Compilation → Scripting Define Symbols | |
커스텀 전처리만들어두고 에디터 상에서 테스트코드 블럭 만들때 좋음. | |
UNITY_DEBUG 전처리 만들어서 | |
#if UNITY_DEBUG | |
~~ 테스트 코드 | |
#endif | |
------ uGUI Image 강제 리프레쉬 SetAllDirty() 함수 사용하기 ------- | |
------ IAP 인앱결제 ------ | |
Project Settings -> Services -> In-App-Purchasing -> 으로 유니티로 구현하기 | |
----- 디버그 도구 코드로 만들기 ----- | |
private string _stageDebugText = "1"; | |
private bool _showDebug = true; | |
private void OnGUI() { | |
GUILayout.Space(166f); | |
_showDebug = GUILayout.Toggle(_showDebug, $"DEBUG {(_showDebug ? "ON": "OFF")}", new GUIStyle(GUI.skin.textField) { fontSize = 20, alignment = TextAnchor.MiddleCenter }, GUILayout.Height(60), GUILayout.Width(128)); | |
if(_showDebug) { | |
_stageDebugText = GUILayout.TextField(_stageDebugText, new GUIStyle(GUI.skin.textField) { fontSize = 20, alignment = TextAnchor.MiddleCenter }, GUILayout.Height(32), GUILayout.Width(128)); | |
if(GUILayout.Button($"GOTO {_stageDebugText}", new GUIStyle(GUI.skin.button) { fontSize = 20 }, GUILayout.Height(64), GUILayout.Width(128))) { | |
GameStart.Invoke(int.Parse(_stageDebugText)); | |
if(PopupManager.Inst.IsAnyPopupOpen()) { | |
PopupManager.Inst.Release_Popup_Resource(); | |
} | |
} | |
if(GUILayout.Button($"NEXT {StageHelper.GetNextStage()?.GetLevel() ?? -1}", new GUIStyle(GUI.skin.button) { fontSize = 20 }, GUILayout.Height(64), GUILayout.Width(128))) { | |
GameStart.Invoke(StageHelper.GetNextStage()?.GetLevel() ?? -1); | |
if(PopupManager.Inst.IsAnyPopupOpen()) { | |
PopupManager.Inst.Release_Popup_Resource(); | |
} | |
} | |
if(GUILayout.Button($"{(SkinHelper.GetCurrentSkin()?.GetSkinType() ?? SkinType.WHITE).ToString()}\nSKIN", new GUIStyle(GUI.skin.button) { fontSize = 20 }, GUILayout.Height(64), GUILayout.Width(128))) { | |
ChangeSkinMod.Invoke((SkinType) ((int) (SkinHelper.GetCurrentSkin().GetSkinType() + 1)%(System.Enum.GetValues(typeof(SkinType)).Length - 1))); | |
} | |
if(GUILayout.Button($"GPGS\nAchievements", new GUIStyle(GUI.skin.button) { fontSize = 20 }, GUILayout.Height(64), GUILayout.Width(128))) { | |
LoginGooglePlayGames(); | |
Social.ShowAchievementsUI(); | |
} | |
if(GUILayout.Button($"SAVE GAME", new GUIStyle(GUI.skin.button) { fontSize = 20 }, GUILayout.Height(64), GUILayout.Width(128))) { | |
DataManager.SaveData(); | |
} | |
// if(GUILayout.Button($"TEST POPUP", new GUIStyle(GUI.skin.button) { fontSize = 20 }, GUILayout.Height(64), GUILayout.Width(128))) { | |
// PopupManager.ShowToast(ToastType.TO_COMBO, "COMBO", 4); | |
// } | |
// if(GUILayout.Button($"ROTATE\nPIECE", new GUIStyle(GUI.skin.button) { fontSize = 18 }, GUILayout.Height(128), GUILayout.Width(128))) { | |
// RotatePiece.Invoke(1); | |
// } | |
} | |
} | |
----- Data 클래스로 객체 클래스 만들때 데이터 클래스 안에 Create() 함수로 객체 클래스를 만드는 방식은 Data클래스가 없을때 만들어지면 안되는, null일때 체크하기 좋고, | |
데이터가 없을때도 만들어져야 한다면, 접근 한정자 잘 활용하려면 객체 클래스의 생성자에 Data클래스 넣어서 만드는 방식으로 진행하는게 좋아보임 ----- | |
ex) keptPiece = data_.keepPieceData?.Create() ?? null; | |
ex) public Game() : this(new GameData()) {} | |
public Game(GameData data_): this (data_, new PieceProvider()) { } | |
----- 비 소모성 결제로 구글이나 앱 스토어 결제기능 가능 ----- | |
----- git 에서 commit 했는데 작업한거 한 두개 수정해서 다시 커밋해야하는데 기존 커밋에 넣고싶을 때 -------- | |
git add 해서 stash에 넣고 | |
git commit --amend | |
----- string.Format(format, 내용) int to string 1000 to 100.0% 로 표현하는 예시 ----- | |
{0:#\.#\%} \ 붙이면 원하는 문자열 그대로 표현가능 | |
----- 모든 씬의 내용은 prefab 형태로 묶어서 작업을 시작하는 구조로 작업해야 git 협업할때 씬 머지 에러를 최대한 방지할 수 있다. ------ | |
----- git flow 가장 간단하게 운용하는 방식 ------ | |
계속 유지하는 브랜치는 딱 2개 | |
1. main (최신 데이터 or release 버전 포함, 돌아가는 가장 최신 정보만 푸쉬) | |
2. develop (개발 중) | |
develop에서 브랜치 따오고 | |
3. feature/??? 브랜치 - 이 브랜치는 기능이 머지 PR이 끝나면 제거 | |
웬만하면 feature(기능) 브랜치간엔 커플링이 없어야 함. | |
a라는 기능이 있으면 feature/a 이름으로 따옴. | |
a랑 연관된 b기능이면 feature/a에서 따와도 됨. | |
4. github 사용 시 | |
- 먼저 개발할 내용을 issue로 기능을 만들고 | |
- feature/a 브랜치 따고 | |
- PR로 코드리뷰후 머지하면서 Squash 머지 같은걸로 commit 깔끔하게 관리하고 feature 브랜치 없앰 | |
5. 버전은 github일시 tag를 만들어서 1.1.0 release 기능 사용해서 원하는 데이터 받을 수 있게 하면 됨. | |
6. hotfix가 필요한 경우 | |
main브랜치에 먼저 적용하고 develop 브랜치에도 그다음 적용 후 제거한다. | |
----- AppLovin Integration 추천 Partner 리스트(2023.12 기준) ----- | |
1. Ad Mob | |
2. Facebook (Meta) | |
3. Unity Ads | |
------ 유니티 스크립트 길이가 길어지면 Component 패턴으로 가독성 좋게 만들 필요가 있을것 같다. ------ | |
InputManager 라는 스크립트가 있으면 | |
1. InputManager/Components 라는 폴더 만들고, | |
2. InputManagerState - 변수선언부 | |
3. InputManagerLogic - 함수구현부인데 여기서 InputManager를 변수로 받아와서 구현시켜주기 (많으면 더 여러개로 나누기) | |
InputManager에는 유니티 함수들만 남긴다. | |
이런식으로 | |
public class InputManagerState | |
{ | |
public PlayerInput input; | |
public bool isTouching = false; | |
public GameController GC; | |
} | |
public class InputManagerLogic | |
{ | |
public InputManager b; | |
public InputManagerLogic(InputManager b){ | |
this.b = b; | |
} | |
// public GameController b.states.GC { | |
// get { return b.states.GC; } | |
// } | |
public void OnTouchDown(InputAction.CallbackContext context) { | |
if(!b.states.GC.game.IsPlaying()) { | |
b.states.isTouching = false; | |
return; | |
} | |
if(b.states.GC.game.IsPieceSelected()||b.states.GC.game.GetCurrentBoard().IsRunAnim()||(b.states.GC.gameUI&&b.states.GC.gameUI.GetFSM!=null&&b.states.GC.gameUI.GetFSM.State != StageGameState.IDLE)) { | |
return; | |
} | |
b.states.isTouching = true; | |
//PIECE CHECK FIRST | |
b.states.GC.game.gameUI.pieceAreas.ToList().ForEach(pieceAreaUI => { | |
if(pieceAreaUI) { | |
var pieceUI = pieceAreaUI.GetComponentInChildren<PieceUI>(); | |
if(pieceUI&&pieceAreaUI.IsTouchOver(Vector3.zero)) { | |
b.states.GC.SelectPiece.Invoke(pieceUI); | |
// Debug.Log($"SelectPiece INVOKE {pieceUI.GetPiece().pieceType}"); | |
pieceUI.isTouching = true; | |
b.states.GC.game.gameUI.GetFSM.ChangeState(StageGameState.CHECK); | |
return; | |
} | |
} | |
}); | |
//PIECE AREA CHECK | |
if(!b.states.GC.game.IsPieceSelected()) { | |
b.states.GC.game.gameUI.pieces.ToList().ForEach(pieceUI=> { | |
if(pieceUI) { | |
var touchPos = b.states.input.Touch.TouchPosition.ReadValue<Vector2>(); | |
var worldPos = Camera.main.ScreenToWorldPoint(new Vector3(touchPos.x, touchPos.y, 100f)); | |
// Debug.Log(worldPos); | |
if(pieceUI.IsTouchOver(worldPos)) { | |
b.states.GC.SelectPiece.Invoke(pieceUI); | |
// Debug.Log($"SelectPiece INVOKE {pieceUI.GetPiece().pieceType}"); | |
pieceUI.isTouching = true; | |
b.states.GC.game.gameUI.GetFSM.ChangeState(StageGameState.CHECK); | |
return; | |
} | |
} | |
}); | |
} | |
//KEEP CHECK | |
var keptPieceUI = b.states.GC.game.gameUI.keptPiece; | |
if(keptPieceUI&&!b.states.GC.game.IsPieceSelected()) { | |
var touchPos = b.states.input.Touch.TouchPosition.ReadValue<Vector2>(); | |
var worldPos = Camera.main.ScreenToWorldPoint(new Vector3(touchPos.x, touchPos.y, 100f)); | |
if(keptPieceUI.IsTouchOver(worldPos)) { | |
b.states.GC.SelectPiece.Invoke(keptPieceUI); | |
// Debug.Log($"SelectPiece INVOKE {keptPieceUI.GetPiece().pieceType}"); | |
keptPieceUI.isTouching = true; | |
b.states.GC.game.gameUI.GetFSM.ChangeState(StageGameState.CHECK); | |
} | |
} | |
// Debug.Log($"TouchDown : { b.states.input.Touch.TouchPosition.ReadValue<Vector2>()}"); | |
} | |
public void OnTouchUp(InputAction.CallbackContext context) { | |
// Debug.Log($"TouchUp : { b.states.input.Touch.TouchPosition.ReadValue<Vector2>()}"); | |
if(!b.states.GC.game.IsPlaying()) { | |
b.states.isTouching = false; | |
return; | |
} | |
if(b.states.isTouching&&b.states.GC.game.IsPieceSelected()) { | |
var touchPos = b.states.input.Touch.TouchPosition.ReadValue<Vector2>(); | |
var worldPos = Camera.main.ScreenToWorldPoint(new Vector3(touchPos.x, touchPos.y, 100f)); | |
var hoverPiecePos = Camera.main.ScreenToWorldPoint(new Vector3(touchPos.x, touchPos.y + DataManager.TOUCH_OFFSET_SPACE, 100f)); | |
//BOARD IN HOLDING PIECE | |
if(UIManager.Inst.IsTouchOverBoard(hoverPiecePos)) { | |
if(b.states.GC.game.GetCurrentSelectedPiece().isSetable) { | |
b.states.GC.SetPiece.Invoke(b.states.GC.game.GetCurrentSelectedPiece().pieceUI); | |
// Debug.Log("SetPiece INVOKE"); | |
} else { | |
//CAN'T SET | |
b.states.GC.game.gameUI.GetFSM.ChangeState(StageGameState.IDLE); | |
// Debug.Log("DeSelectPiece"); | |
} | |
//KEEP AREA | |
} else if(b.states.GC.game.gameUI.keepArea.IsTouchOver(worldPos)&&b.states.GC.game.keptPiece==null) { | |
b.states.GC.KeepPiece.Invoke(b.states.GC.gameUI.pieces.Where(pieceUI => (pieceUI) ? true: false).ToList().Find(pieceUI => pieceUI.GetPiece().guid == b.states.GC.game.GetCurrentSelectedPiece().guid)); | |
b.states.GC.game.gameUI.GetFSM.ChangeState(StageGameState.IDLE); | |
} else { | |
b.states.GC.game.gameUI.GetFSM.ChangeState(StageGameState.IDLE); | |
// Debug.Log("DeSelectPiece"); | |
} | |
b.states.isTouching = false; | |
} else { | |
//NOT SELECTED | |
b.states.isTouching = false; | |
return; | |
} | |
} | |
} | |
public class InputManager : MonoSingleton<InputManager> | |
{ | |
public InputManagerState states; | |
public InputManagerLogic logics; | |
public InputManager(){ | |
states = new InputManagerState(); | |
logics = new InputManagerLogic(this); | |
} | |
override protected void Awake() { | |
base.Awake(); | |
states.input = new PlayerInput(); | |
} | |
private void Start() { | |
states.input.Touch.TouchPress.started += ctx => logics.OnTouchDown(ctx); | |
states.input.Touch.TouchPress.canceled += ctx => logics.OnTouchUp(ctx); | |
states.GC = GameController.Inst; | |
} | |
private void OnEnable() { | |
states.input.Enable(); | |
} | |
private void OnDisable() { | |
states.input.Disable(); | |
} | |
} | |
------ Android iOS 개발 시 프레임 vSync = 0, targetFrame = 60 적용했지만 30프레임 나올 경우 ------- | |
2022버전에 Project Settings -> Project -> Adaptive Performance -> Initialize Adaptive Performance on Startup 체크 해제, | |
그 아래 Providers 다 체크 해제 하면 됨. | |
----- Android 개발할때 apk 빌드해서 압축한다음 카카오톡으로 보내는 작업하기보다 컴터랑 폰 연결해놓고 adb로 설치하는식으로 작업하면 로그도 보고 좋다. ------ | |
vscode로 사용시 | |
.\adb install D:\UnityProject\FourTris\Build\alpha-266.apk (설치) | |
.\adb logcat -s Unity (실시간 로그볼 수 있음) | |
PackageManager로 Android Logcat 설치해서 더 편하게 가능. | |
1. 폰에 Debug 모드 활성화 해야함 | |
- Settings 앱을 엽니다.(Android 8.0 이상에만 해당) | |
System을 선택합니다. | |
- 아래로 스크롤하여 About phone을 선택합니다. | |
- 아래로 스크롤하여 Build number를 7번 탭합니다. | |
- 이전 화면으로 돌아가서 아래쪽의 Developer options를 찾습니다. | |
- USB debugging을 활성화 | |
2. 폰 usb충전선으로 연결 (wifi디버깅 기능도 있음) | |
3. adb가 기본 설치 되어있는 파일로 이동(아래는 adb의 디폴트경로) ex) cd C:\Users\admin\AppData\Local\Android\Sdk\platform-tools | |
4. adb install apk경로/apk이름.apk 를 통해서 설치 ex) adb install C:\buildTest\StreamingTest.apk | |
5. Success 나오면 성공 | |
----- TMP_Text Text 길이에 초과하는거에 따라 전체 크기나 마진 다르게 하는 코드 (가운데 정렬때문에 코드로 조절해야 할경우) ------- | |
tT_Title.ForceMeshUpdate(); | |
tT_Title.margin = tT_Title.isTextOverflowing ? new Vector4(0,0,0,0) : new Vector4(100,0,0,0); | |
단 TMP에 overflow랑 wrapping enable로 해놔야 isTextOverflow 체크된다. | |
----- GPGS SDK 로그인 하려면 aab 무조건 올려서 키 두개 넣어야한다. (2023.12기준) ------- | |
------ Github에 100MB 이상 파일을 올리기 위해선 git-lfs 설정을 해야한다. ------ | |
ex) Firebase SDK 설치시 용량 큰 파일이 .bundle, .so 파일이 3개가 생성된다. | |
1. git lfs install (LFS 설치하고) | |
2. git lfs track "*.so" (.so파일 100메가 넘어갈거같은 파일 .gitattribute 등록) | |
2-2. git lfs track "*.bundle" | |
3. git push 하면 끝 | |
이전에 commit 한게 있으면 git reset --soft 로 취소하면 됨. | |
------- SDK Import 순서 -------- | |
1. Firebase | |
- Analytics 필수 | |
- Auth (선택) 게스트 로그인 + 파베에 계정 연동 시킬때 | |
- Messaging (선택) 푸쉬 알람 | |
- DataBase, Firestore (선택) 실시간 데이터베이스 + 등등 | |
2. GPGS | |
3. AppLovin (MAX) | |
4. Hackle | |
5. Apple | |
------ 연산자 오버라이딩 Null 처리 ------ | |
public static bool operator ==(Block lhs, Block rhs) { | |
if (object.ReferenceEquals(lhs, null)) | |
return object.ReferenceEquals(rhs, null); | |
else if (object.ReferenceEquals(rhs, null)) | |
return false; | |
return lhs.Equals(rhs); | |
} | |
public static bool operator !=(Block lhs, Block rhs) { | |
if (object.ReferenceEquals(lhs, null)) | |
return !object.ReferenceEquals(rhs, null); | |
else if (object.ReferenceEquals(rhs, null)) | |
return true; | |
return !lhs.Equals(rhs); | |
} | |
public override bool Equals(object obj) { | |
if (obj == null) | |
return false; | |
if (obj is not Block value) | |
return false; | |
return this.Hash == value.Hash; | |
} | |
public override int GetHashCode() | |
{ | |
return Hash; | |
} | |
------ Hierarchy(하이라키)에서 검색할 때 필터링 t:~~~ 컴포넌트 찾는거 스크립트(확장자미포함)도 찾을 수 있다. ref:~~~ 도 있음. | |
ex) ref:Assets/04_Image/Anim/Bouquet_00.png ref:script절대경로(확장자포함) ref는 확장자 포함해야함. | |
ex) t:AScript AScript 컴포넌트 들어간 모든 부분 검색가능 | |
------ git submodule / git subtree로 레파지토리 안에 프레임워크 레파지토리 집어넣는식으로 구성할 수 있음. ------- | |
------- SDK 붙일때 dll 파일이 겹쳐서 build시 에러가 나는 경우가 생김, 예를 들면 앱러빈하고 핵클 붙일때 겹침, 이때 핵클 import시 겹쳐서 생성 되는 dll 파일을 지우면 에러 해결 됨. ----- | |
-------- GruopBy 로 키 anonymous type (new {~~~, ~~~~} )쓰면 매개변수에 dynamic으로 적으면 됨 ------- | |
public IEnumerable<IGrouping<dynamic, BaseMarble>> GetMergeableMarbles() { | |
return marbles.GroupBy(marble => new {marble.slug, marble.MarbleLevel})?.Where(group => group.Count() >= 3 && group.Key.MarbleLevel != CardLevel.GOLD) ?? null; | |
} | |
-------- Ticker -------- | |
빨간점으로 표기해주는 거 용어 가장 좋은건 Ticker이다. RedDot, Ticker, BreakPoint 라고 표현 | |
------- 트윈 시퀀스 예제 ----- | |
using DG.Tweening; | |
//DoTween Sequence | |
Sequence seq = DOTween.Sequence(); | |
seq.Append(animation); // animation 바로 실행 | |
seq.Append(animation2); // animation 완료 후 animation2를 실행 | |
seq.AppendInterval(1.0f); // 1초를 기다림 | |
seq.AppendCallback(myCallback); // animation2 완료 후 1초를 기다렸다 myCallbak 호출 | |
seq.Join(animation3); // animation과 동시에 실행 | |
seq.Prepend(animation4); // 맨 처음에 실행 | |
seq.Insert(1.0f, animation5); // 1초 후 animation5 실행 | |
seq.InsertCallback(1.0f, myCallback); // 1초후 myCallback 호출 | |
// 람다식 콜백 적용 | |
seq.InsertCallback(1.0f, () => { Debug.Log("1초 후 실행" }); | |
seq.Play(); // Sequence 실행 | |
------- Enum에 DEFUALT라는 단어를 넣어서 ToString으로 바꾼다음 GetField()로 변수를 가져오려고하면 Null 값을 먹인다. ------- | |
------- DOTween 정리 ------- | |
Tween.Duration 으로 총 걸리는시간 구할 수 있음. | |
Tween.Elapsed 으로 지금까지 진행된 시간을 구할 수 있음. | |
await Tween.AsyncWaitForCompletion 으로 완료될때까지 대기할 수 있음. Task 리턴 | |
TweenParams tParms = new TweenParams().SetLoops(-1).SetEase(Ease.OutElastic); | |
트윈 설정을 저장해두고 프리셋처럼 쓸 수 있다. | |
// Apply them to a couple of tweens | |
// SetAs함수를 통해 미리 설정해놨던 사항을 적용 시킨다. | |
transformA.DOMoveX(15, 1).SetAs(tParms); | |
transformB.DOMoveY(10, 1).SetAs(tParms); | |
DOTween.TweensByTarget(trans_, true) 첫 번째 오브젝트, 두 번째 매개변수 실행되고 있는 트윈만 가져올지 불린 | |
현재 오브젝트에 돌고 있는 Tween을 다 가져오는 함수를 이용해서 애니메이션 실행 되는지 체크하는 코드도 가능하다. | |
다만 하위 오브젝트에 돌고 있는 Tween은 안가져오는 걸로 판단되어 재귀형태로 함수를 만들어서 찾아오는 식으로 구현하면 하위 오브젝트에 달려있는 모든 애니메이션 체크도 가능할듯? | |
참고로 Transform을 체크해도 해당 오브젝트의 Material에 적용된 Tween도 못 가져오니까 달려있는 컴포넌트 전부를 체크하게 하는식으로 해야한다. | |
아래는 내가 사용한 예 | |
public bool IsAnyRunAnimationWithChilds(Transform trans_) { | |
bool result = IsRunAnimation(trans_); | |
SpriteRenderer selfM = trans_?.GetComponent<SpriteRenderer>() ?? null; | |
if(selfM) { | |
result = result || IsRunAnimation(selfM.material); | |
result = result || IsBlockShaderChanged(selfM.material); | |
} | |
for(int i=0; i<trans_.childCount; i++) { | |
result = result || IsAnyRunAnimationWithChilds(trans_.GetChild(i)); | |
} | |
return result; | |
} | |
public bool IsRunAnimation(object trans_) { | |
return (DOTween.TweensByTarget(trans_, true)?.Count() ?? 0) > 0; | |
} | |
------- Dictionary Linq로 Values 한번에 수정하는 방법 ------- | |
successCondition.ToDictionary(el => el.Key, el => 0); 모든 Value를 0으로 만든 Dictionary 를 return한다. | |
------- Dictionary lambda 형태로 초기화 하는 방법 ------ | |
var dic = new Dictionary<SuccessCondition, int>() { {SuccessCondition.SCORE, 0}, {Suc... , 13} }; | |
이런식으로 선언 즉시 초기화 가능. | |
------- Resources 폴더 말고 다른 폴더에서 데이터 불러오기 (에디터 한정) -------- | |
public static List<T> FindAssets<T>(params string[] paths) where T : UnityEngine.Object | |
{ | |
string[] assetGUIDs = AssetDatabase.FindAssets("t:" + typeof(T).Name, paths); | |
List<T> assets = new List<T>(); | |
foreach (string guid in assetGUIDs) | |
{ | |
string assetPath = AssetDatabase.GUIDToAssetPath(guid); | |
T asset = AssetDatabase.LoadAssetAtPath<T>(assetPath); | |
assets.Add(asset); | |
} | |
return assets; | |
} | |
------- lambda로 List<Class> 쉽게 중복제거하기 ------ | |
_removeBlocks.GroupBy(block => block.pos).Select(block => block.First()).ToList(); | |
------- Enum과 Event기반으로 설계하는 건 정말 필요하다. ------ | |
------- C#에서 delegate에 등록된 모든 함수 가져오는 함수 GetInvocationList() 이걸로 가져와서 UnityEvent의 RemoveAllListener() 처럼 제거할 수 있다. ----- | |
------ Android 빌드랑 테스트 시 여전히 Android Studio를 설치하고 | |
SDK Manager에 들어가서 SDK 버전 다운받은다음 폴더채로 유니티 안드로이드 SDK있는 폴더에 넣어줘야함. | |
테스트는 Unity Remote 5 설치랑 Edit > Project Settings > Editor > Any Android 는 기본이고 SDK MANAGER에 Google USB Driver 를 설치해야한다. | |
------- ScriptableObject 파일이름 자동변경 ------- | |
public bool renameOnValidate = false; | |
public int id; | |
public int itemName | |
private void OnValidate() | |
{ | |
if (renameOnValidate) | |
{ | |
string thisFileNewName = id + "_" + itemName; | |
string assetPath = UnityEditor.AssetDatabase.GetAssetPath(this.GetInstanceID()); | |
UnityEditor.AssetDatabase.RenameAsset(assetPath, thisFileNewName); | |
renameOnValidate = false; | |
} | |
} | |
OnValidate()는 ScriptableObject 에서 실행시킬 수 있는 함수 수정될 때 마다 실행되는 유니티 이벤트 함수다 | |
이런식으로 코드 넣으면 ScriptableObject 파일 이름 설정 내용 활용해서 자동으로 바꿀 수도 있다. | |
------ SerializedCollections 잘 활용해서 Dictionary 같은것도 인스펙터에 쉽게 띄워줄 수 있다. ------ | |
assetstore에도 무료 에셋 많음. | |
------ Assembly Definition 최상 위에 만든다음 Reference 설정하는 부분에 필요한 Assembly만 추가하면 속도가 엄청 향상된다. ------ | |
매우 중요한 스킬인거 같다. 코드 수정할때 40초 정도 기다려야하는거 5초도 안걸림 | |
------ Timestamp 찍기 ------ | |
createTime = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds(); | |
createTime = ((DateTimeOffset)DateTime.UtcNow.AddHours(9)).ToUnixTimeSeconds(); | |
타임스탬프는 서버에서 가져와서 비교할때 많이 쓰기에 알아둬야함. | |
Now 는 로컬타임 (한국에선 한국, 다른나라에선 다른 시간) | |
UtcNow + 9 는 한국시간 | |
------- GIT Private 프로젝트 받기 ------- | |
1. ssh-keygen Enter로 생성 | |
2. git settings 가서 SSH keys 의 New SSH key 에 .pub 붙어있는 파일에 내용 전부 복사 public key 복사 후 | |
3. git clone ~~~.git 으로 프로젝트 받기 | |
------- URP TMP_Text 와 TMP_Text(UI) 색 표현이 다를 때 Multifly 되는 현상일 경우 해결방법------- | |
2021.3.9f1 버전에서 그런 현상발생 2022.3.5f1 버전에도 여전하다. (TMP 3.0.6 버전에서 여전하다.) | |
TMP Text 색을 지정하면 Gamma Space에서 지정한 색처럼 이상하게 나오는 현상이 발생한다. TMP Text (UI)에서는 괜찮은데 | |
TMP_Text가 MeshRenderer안에 들어 있는 메터리얼의 Shader-TextMeshPro/Distance Field의 Face의 흰색과 곱해져서 원하는 색이 안나오는 문제가 생김. | |
1. Gradient 옵션을 켜서 코드로 접근해 Single로 같은 색을 지정시켜준다. | |
TMP_Text(UI)는 같은 조건으로 테스트하면 원하는 색이 나오는데 이럴 경우 편법으로 TMP_Text의 Color Gradient 를 체크하고 Color Mode - Single, | |
Colors를 동일한 색으로 설정하면 곱해져서 원하는 색이 제대로 나오는 식으로 처리가 가능하다. | |
2. TMP_SDF.shader 코드를 수정한다. - 이거 추천 한줄만 바꾸면 해결 됨. | |
faceColor.rgb *= input.color.rgb; 이 한줄을 주석처리하고 (* PixShader { } 안에 있다.) | |
faceColor.rgb *= fixed4(GammaToLinearSpace(input.color.rgb), faceColor.a); 이렇게 변경하면 된다. 완벽하게 수정하는 방법 | |
단 2022.3.5f1 버전이후면 canvas에 Color Always in Gamma Color 체크 | |
리니어 스페이스를 추천하면서 이런 심각한 문제를 방치하는 유니티가 밉다... 한 줄만 변경해주면 되는 코드를 핫픽스 안해주다니.. | |
------- 안드로이드 빌드 유니티 버전 업해서 gradle 문제가 있을경우 최신 SDK 업데이트가 되어있는지 체크해야한다. ------ | |
최소 버전을 최대로 높이면 유니티가 알아서 SDK 최신버전 받아오는데 그런식의 편법이든 SDK 버전을 올려야할 수 있다. | |
------- 화면 녹화 gif 녹화 필요할 때 ------- | |
외부 프로그램 쓰지말고 2022.1 이상 버전이면 Package Manager에서 설치할 수 있는 유니티 내에서 지원하는 Unity Recoder를 사용하면 된다. | |
gif | |
mp4 | |
png | |
jpg 등 세부기능 까지 다 지원 됨. | |
------ TMP Text Gamma 색 이 TMP Text (UI) Linear 랑 다르게 나올 때------ | |
2022.3 버전의 Canvas의 달려있는 vertexColorAlwaysGammaSpace 를 끄면 색이 정상으로 나온다. | |
아니면 https://gist.github.com/lukz/0065cb4a18242aa94633ac02789234a3 이걸 적용해도 된다. | |
------ UniRx 몇개 추가------- | |
0. UniRx 쓸대 습관적으로 .TakeUntilDisable(this) 붙여주자 아니면 메모리에 계속 남아있는경우가 생길 수 있다. | |
AddTo(this)사용보다 이게 나은 이유는 TakeUntilDisable(this)는 Disable 되거나 Destroy 될때 둘 다 제거하는 기능이니까 | |
사실 상 스크립트는 Diabled될 때 작동 안되는게 정상인데 작동되면 안되니까 AddTo(this) 사용하지말고 TakeUntilDisable(this)를 쓰자 | |
혹시라도 Mono가 없을경우 .TakeWhile(_ => isRunFlag) 이런식으로 TakeWhile로 생존주기를 정할 수 있으니까 이것도 유용함 | |
0-2. UniRx 쓸때 그냥 무의식적으로 Timer만 쓰시는 분 있는데 ~~~Frame 붙은걸로 사용해야 엔진시간으로 처리할 수 있다. | |
ex) Observable.TimerFrame(Mathf.RoundToInt(원하는 시간초/Time.deltaTime)).Subscribe(_ => { 블라블라 }).TakeUntilDisable(this); Frame 쓸때는 이런식으로 입력하면 됨. | |
1. 버튼 클릭시 다음 클릭이 0.5f 이전까지 클릭 무시 | |
public Button agreeButton; | |
void Awake() | |
{ | |
agreeButton. | |
OnClickAsObservable(). | |
ThrottleFirst(System.TimeSpan.FromSeconds(0.5f)). | |
Subscribe(_x => | |
{ | |
//실행 코드 | |
}); | |
} | |
2. 반복문 (For, Foreach, ForEach 등등) 쓰다가 가끔 딜레이 줘서 처리해야하는데 코루틴 쓰기 귀찮은 경우 (엄청 많음) | |
ex) foreach(var btn in eventCtrl.buttonGP.GetComponentsInChildren<CanvasGroup>()) { | |
btn.DOFade(1.0f, 0.4f).OnComplete(() => { | |
btn.interactable=true; | |
btn.blocksRaycasts=true; | |
}); | |
await Observable.Timer(TimeSpan.FromSeconds(3)).TakeUntilDisable(this); //이거 하나만 추가하면 3초간격으로 반복 (단 유니티시간아님) | |
await Observable.TimerFrame(Mathf.RoundToInt(1.0f/Time.deltaTime)).TakeUntilDisable(this); // 이렇게 하면 유니티시간 | |
} | |
3. 리스트나 딕셔너리 쓰다가 이벤트로 추가/제거 시점에서 처리하고 싶을 때 따로 클래스 만들어서 할 필요 없이 가능 | |
var collection = new ReactiveCollection<string>(); | |
collection | |
.ObserveAdd() | |
.Subscribe(x => | |
{ | |
Debug.Log($"Add [{x.Index}] = {x.Value}"); | |
}); | |
collection | |
.ObserveRemove() | |
.Subscribe(x => | |
{ | |
Debug.Log($"Remove [{x.Index}] = {x.Value}"); | |
}); | |
collection.Add("Apple"); | |
collection.Add("Baseball"); | |
collection.Add("Cherry"); | |
collection.Remove("Apple"); | |
------- UnityUIExtensions Avatar UnityUIExtensions ---------- | |
uGUI확장 기능들 무료인데 유용한것들 많음. | |
에셋스토어에서는 10달러 얘네 사이트에서 git이나 패키지로 받으면 무료 사용해보길 | |
https://assetstore.unity.com/packages/2d/gui/ui-extensions-175295 | |
패키지 매니저에서 추가할 경우 > https://bitbucket.org/UnityUIExtensions/unity-ui-extensions.git | |
------- 간단하게 Coroutine 사용할 때 팁 ------- | |
함수 선언 시 void 대신 IEnumerator 해주고 | |
for(float t = 0; t<playTime; t+=Time.deltaTime) { | |
yield return null; | |
} | |
animatoincurve Evaluate(t) Ease 효과 응용가능 | |
함수 실행할때는 StartCoroutine(선언한 함수); | |
------ 프로퍼티 쓸 때 주의해야할 점 -------- | |
set 할 때 private 함수에 집어넣는게 아니라 public 프로퍼티에 집어넣으면 무한반복이 발생해서 크래쉬 에러조차 없이 꺼진다. 조심해야함. | |
------- ♥♥♥♥ 플레이모드 도중에 스크립트 수정해도 다시 껐다 킬 필요없는 방법 Hot Reload Fast Script Reload ------- | |
[필수] | |
와 시바... 이건 지금까지 몰라서 눈물나는 정보 | |
오픈소스 ㅠㅠㅠ 이건 작업속도 5배 정도 향상시킬수있다 레알... | |
Preferences > Asset Pipeline > Auto Refresh > Disable -> Enabled Outside Playmode 체크 | |
에셋을 사용하지 않고 적용하는 방법 | |
Preferences > General > Script Changes While Playing > Stop And ~~ -> 에서 Recompile And Continue ~~~ 로 변경 | |
Project Settings > Player > Resolution > Run In Background 체크 | |
------- timeline 동적으로 track header component 집어 넣는 코드 ---------- | |
public void AutoConnectManagerSceneComponents() { | |
var timelineAsset = director.playableAsset as TimelineAsset; | |
foreach(var track in timelineAsset.GetOutputTracks()) { | |
//FADE IN OUT IMAGE | |
if(track.GetType() == typeof(ScreenFaderTrack)) { | |
director.SetGenericBinding(track, UIManager.Inst.fadePanel); | |
//SIGNAL TRACK | |
} else if(track.GetType() == typeof(UnityEngine.Timeline.SignalTrack)) { | |
director.SetGenericBinding(track, GameObject.FindGameObjectWithTag("GameManager").GetComponent<SignalReceiver>()); | |
} | |
// Debug.Log(track.GetType()); | |
} | |
} | |
-------- 텍스쳐가 겹쳐서 반짝반짝거리는 현상 Z-Fighting -------- | |
-------- Script Icon 변경하는 방법 ------ | |
Project에서 스크립트 파일 클릭 후 Inspector에서 Icon 클릭해서 Other로 Icon 변경가능 | |
--------- 상속받은 클래스의 인스턴스의 타입을 알아야 할 때 --------- | |
baseMob 같은 클래스를 상속받아서 mob1, mob2 이런 클래스를 만들었고, 인스턴스를 체크해야할 경우 이런 경우 많다. | |
ex) | |
if(typeof(EliteRoom).IsInstanceOfType(currRoom)) { | |
currRoom 라는 BaseRoom 변수안에 연결된 인스턴스를 체크하는 예제 EliteRoom일 때만 실행 | |
} | |
------- Steamworks.NET 스팀 연동 -------- | |
1. 편한방법 | |
스팀웍스 SDK가 C#부분에 대한 정보도 없고 어려워서 타 개발자의 도구를 사용한다. | |
https://github.com/Facepunch/Facepunch.Steamworks | |
2. 정석 | |
스팀웍스 github.io 홈페이지에서 받아서 적용 | |
package 매니저 사용하는게 난 좋다고 본다 버전확인도 가능하니까 | |
https://steamworks.github.io/installation/#unity-instructions | |
# 기본정보 | |
SteamFriends.GetPersonaName() 로그인된 계정 닉네임 | |
SteamUser.GetSteamID() 로그인된 계정 ID | |
# Achievements Steamworks.SteamUserStats.GetAchievement 로 out변수로 체크여부 가져와서 안되어있으면 Set으로하고 SaveStats 하면됨. | |
# SteamFriends.ActivateGameOverlay("url_or_menu_command"); //오버레이 여는 | |
# 오버레이 켜지는 것 같은 기능은 Playmode 에서는 테스트 불가능하다. 따라서 Build해서 테스트 가능. 다만 오버레이 기능 자체는 Playmode에서도 되는 것을 체크함. | |
SteamFriends.SetOverlayNotificationPosition(NotificationPosition) | |
게임 오버레이의 알림 위치를 설정합니다. NotificationPosition은 ENotificationPosition 열거형으로 지정합니다. | |
SteamFriends.SetOverlayNotificationInset(HorizontalInset, VerticalInset) | |
게임 오버레이의 알림 위치를 상대적으로 조정합니다. HorizontalInset과 VerticalInset은 각각 수평과 수직으로 알림 위치를 조정하는 값을 지정합니다. | |
SteamFriends.ActivateGameOverlayToWebPage(URL) | |
Steam 웹 브라우저를 사용하여 지정된 URL의 웹 페이지를 엽니다. | |
SteamFriends.ActivateGameOverlayToStore(AppID, Flag) | |
Steam 스토어에서 AppID로 지정된 게임의 페이지를 엽니다. | |
SteamFriends.ActivateGameOverlayInviteDialog(LobbyID) | |
Steam 초대 대화 상자를 엽니다. | |
SteamFriends.ActivateGameOverlayInviteDialogConnectString(CallbackSettings) | |
Steam 초대 대화 상자를 열고, 다른 유저가 게임에 참여할 수 있는 연결 문자열을 표시합니다. | |
SteamFriends.ActivateGameOverlayToUser(Context, SteamID) | |
지정된 Steam ID를 가진 사용자의 프로필 페이지를 엽니다. | |
SteamFriends.OnGameOverlayActivated(IsActive) | |
게임 오버레이가 활성화되거나 비활성화될 때 호출되는 콜백 함수입니다. | |
## Auto Cloud Save & Load 코드 | |
using UnityEngine; | |
using Steamworks; | |
public class SteamCloudSave : MonoBehaviour | |
{ | |
private bool m_IsCloudEnabled; | |
private void Start() | |
{ | |
// Steam 초기화 | |
SteamClient.Init(480); | |
// Steam 클라우드 사용 가능 확인 | |
m_IsCloudEnabled = SteamRemoteStorage.IsCloudEnabledForAccount(); | |
// Steam 클라우드 활성화 | |
if (m_IsCloudEnabled) | |
{ | |
SteamRemoteStorage.CloudEnabledForApp(true); | |
} | |
} | |
private void OnDestroy() | |
{ | |
// Steam 클라이언트 종료 | |
SteamClient.Shutdown(); | |
} | |
private void OnApplicationQuit() | |
{ | |
// 어플리케이션이 종료될 때, 클라우드에 저장 | |
if (m_IsCloudEnabled) | |
{ | |
SteamRemoteStorage.FileWriteStreamOpen("save_data.txt"); | |
string data = "Save data here..."; | |
byte[] bytes = System.Text.Encoding.ASCII.GetBytes(data); | |
SteamRemoteStorage.FileWriteStreamWriteChunk(bytes, bytes.Length); | |
SteamRemoteStorage.FileWriteStreamClose(); | |
} | |
} | |
private void OnApplicationFocus(bool hasFocus) | |
{ | |
// 어플리케이션이 포커스를 잃을 때, 클라우드에서 로드 | |
if (!hasFocus && m_IsCloudEnabled) | |
{ | |
SteamRemoteStorage.FileReadAsync("save_data.txt", OnCloudLoadResult); | |
} | |
} | |
private void OnCloudLoadResult(RemoteStorageFileReadAsyncComplete_t result, bool bIOFailure) | |
{ | |
if (result.m_eResult == EResult.k_EResultOK && !bIOFailure) | |
{ | |
byte[] data = new byte[result.m_nBytesRead]; | |
SteamRemoteStorage.FileReadAsyncComplete(result.m_hFileReadAsync, data, result.m_nBytesRead); | |
string loadedData = System.Text.Encoding.ASCII.GetString(data); | |
// 로드한 데이터를 처리합니다. | |
} | |
} | |
} | |
최대 유저당 100GB / 1만개 파일 수 | |
------ 고유 아이디 UUID 부여하는 코드 -------- | |
public static string UUIDGenerate() { | |
return Guid.NewGuid().ToString("N"); | |
} | |
--------- default(Vector3) , default(Vector2) 이런식으로 매개변수에 default 값을 Vector3 변수로 주고싶을때 사용 ---------- | |
ex) public void ShowFloatingText(int value_, CreatureObject target_ = null, DamageTextType damageType_ = DamageTextType.NORMAL, Vector2 offset_ = default(Vector2)) { | |
} | |
--------- OnDrag, OnEngDrag ... New Input System에서는 안먹음 -------- | |
------- .Net Json Vector3 안되니까 Vector3Int 사용하면 됨 ----- | |
Formatting.Indented 옵션으로 pretty print 기능 표현할 수 있음. | |
------ List<~~> -------- | |
JsonUtility ToJson Serialize 안되는 문제 | |
using System.Collections.Generic; | |
using UnityEngine; | |
[System.Serializable] | |
public class SerializableList<T> { | |
public List<T> list; | |
} | |
public class ListToJsonExample : MonoBehaviour { | |
[SerializeField] private SerializableList<string> names; | |
private void Awake() { | |
names.list.Add("Mark"); | |
names.list.Add("Luke"); | |
string json = JsonUtility.ToJson(names); | |
Debug.Log(json); | |
} | |
} | |
이런식으로 해결가능 | |
----- .Net Json vs JsonUtility ----- | |
mono상속받은 클래스는 json화 불가능 | |
public string ObjectToJson(object obj) { | |
return JsonConvert.SerializeObject(obj); | |
} | |
public T JsonToObject<T>(string jsonData) { | |
return JsonConvert.DeserializeObject<T>(jsonData); | |
} | |
JsonUtility 많이 좋아져서 이거 쓰는게 좋아보이는듯 Vector3 되는것부터 Mono Class 되는거 사기임 | |
JsonUtility.FromJson | |
Json string을 받아서 새 object를 생성해서 리턴해주는 함수다. | |
object의 class는 [System.Serializable] 를 지원해야하고, 멤버는 public이어야 한다. | |
일반 class와 struct만 지원한다. (MonoBehaviour, ScriptableObject 지원 안함.) | |
JsonUtility.FromJsonOverwrite | |
Json string과 기존의 object를 받아서, 기존의 object에 Json 데이터를 덮어씌워주는 함수다. | |
object의 class는 [System.Serializable] 를 지원해야하고, 멤버는 public이어야 한다. | |
일반 class와 struct뿐만 아니라, MonoBehaviour, ScriptableObject도 지원한다. | |
JsonUtility.ToJson | |
object를 받아서, object의 정보를 Json string으로 생성해서 리턴해주는 함수다. | |
------- AES encryption 암호화 방법 --------- | |
using System.Security.Cryptography; | |
public class Program | |
{ | |
static void Main() | |
{ | |
// Serialize an object to JSON | |
Person person = new Person { Name = "John", Age = 30 }; | |
string json = JsonConvert.SerializeObject(person); | |
// Encrypt the serialized JSON data | |
string key = "mykey"; | |
byte[] encryptedData = Encrypt(json, key); | |
Console.WriteLine("Encrypted data: " + Convert.ToBase64String(encryptedData)); | |
// Decrypt the encrypted data | |
string decryptedJson = Decrypt(encryptedData, key); | |
Person deserializedPerson = JsonConvert.DeserializeObject<Person>(decryptedJson); | |
Console.WriteLine("Name: " + deserializedPerson.Name + ", Age: " + deserializedPerson.Age); | |
} | |
public static byte[] Encrypt(string text, string key) | |
{ | |
Aes aes = Aes.Create(); | |
aes.Key = Encoding.UTF8.GetBytes(key); | |
aes.IV = new byte[16]; | |
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV); | |
byte[] plainText = Encoding.UTF8.GetBytes(text); | |
byte[] cipherText = encryptor.TransformFinalBlock(plainText, 0, plainText.Length); | |
return cipherText; | |
} | |
public static string Decrypt(byte[] cipherText, string key) | |
{ | |
Aes aes = Aes.Create(); | |
aes.Key = Encoding.UTF8.GetBytes(key); | |
aes.IV = new byte[16]; | |
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV); | |
byte[] plainText = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length); | |
string text = Encoding.UTF8.GetString(plainText); | |
return text; | |
} | |
} | |
public class Person | |
{ | |
public string Name { get; set; } | |
public int Age { get; set; } | |
} | |
using UnityEngine; | |
using UnityEngine.UI; | |
using System.IO; | |
using System.Security.Cryptography; | |
using System.Text; | |
using Newtonsoft.Json; | |
public class Example : MonoBehaviour | |
{ | |
// UI objects to display the encrypted and decrypted data | |
public Text encryptedText; | |
public Text decryptedText; | |
// A sample data object to be serialized and encrypted | |
public DataObject dataObject; | |
// The encryption key | |
private string encryptionKey = "myEncryptionKey"; | |
void Start() | |
{ | |
// Serialize the data object to JSON | |
string jsonData = JsonConvert.SerializeObject(dataObject); | |
// Encrypt the JSON data | |
string encryptedJsonData = EncryptString(jsonData, encryptionKey); | |
// Display the encrypted data | |
encryptedText.text = "Encrypted data:\n" + encryptedJsonData; | |
// Decrypt the encrypted data | |
string decryptedJsonData = DecryptString(encryptedJsonData, encryptionKey); | |
// Deserialize the decrypted JSON data into a data object | |
DataObject decryptedDataObject = JsonConvert.DeserializeObject<DataObject>(decryptedJsonData); | |
// Display the decrypted data | |
decryptedText.text = "Decrypted data:\n" + decryptedDataObject.ToString(); | |
} | |
// Encrypts a string using AES encryption | |
private string EncryptString(string input, string key) | |
{ | |
byte[] inputBytes = Encoding.UTF8.GetBytes(input); | |
byte[] keyBytes = Encoding.UTF8.GetBytes(key); | |
byte[] ivBytes = new byte[16]; | |
AesManaged aes = new AesManaged(); | |
aes.Key = keyBytes; | |
aes.IV = ivBytes; | |
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV); | |
MemoryStream ms = new MemoryStream(); | |
CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write); | |
cs.Write(inputBytes, 0, inputBytes.Length); | |
cs.FlushFinalBlock(); | |
byte[] cipherBytes = ms.ToArray(); | |
cs.Close(); | |
ms.Close(); | |
return Convert.ToBase64String(cipherBytes); | |
} | |
// Decrypts a string using AES encryption | |
private string DecryptString(string input, string key) | |
{ | |
byte[] inputBytes = Convert.FromBase64String(input); | |
byte[] keyBytes = Encoding.UTF8.GetBytes(key); | |
byte[] ivBytes = new byte[16]; | |
AesManaged aes = new AesManaged(); | |
aes.Key = keyBytes; | |
aes.IV = ivBytes; | |
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV); | |
MemoryStream ms = new MemoryStream(inputBytes); | |
CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); | |
byte[] plainBytes = new byte[inputBytes.Length]; | |
int decryptedByteCount = cs.Read(plainBytes, 0, plainBytes.Length); | |
cs.Close(); | |
ms.Close(); | |
return Encoding.UTF8.GetString(plainBytes, 0, decryptedByteCount); | |
} | |
} | |
// A sample data object to be serialized and encrypted | |
public class DataObject | |
{ | |
public string Name; | |
public int Age; | |
public string Address; | |
public override string ToString() | |
{ | |
return "Name: " + Name + ", Age: " + Age + ", Address: " + Address; | |
} | |
} | |
------- .NET JSON 유니티 패키지매니저에 추가하는 방법 -------- | |
Add Name으로 com.unity.nuget.newtonsoft-json 입력하면 됨. | |
-------- IPointerEnter, ~~~Exit 부정확할때 있음 CollsionEnter, Exit도 ------- | |
따라서 Raycaster로 값받아와서 오브젝트 위에있는지 계속체크하는거나 Stay 같은거 쓰는게 정신건강에 좋음. | |
------- DOTween 쓸 때 팁 ------- | |
DOTween으로 애니메이션 줄 때 중복으로 처리되는 게 걱정되면 아래와같이 되돌리거나 제거하고 진행하면 좋다. | |
DORewind()나 DOKill() 잘 활용하기 | |
~~~.DORewind() | |
~~~.DO~~~(하고싶은거) | |
단, 아무때나 무작정 사용하는게 아니라 되돌려도 이상하지 않을 때만 DORewind 스케일 값을 건드릴때 무한 반복의 원인이 될 수 있음) | |
이어서 진행해야할 경우엔 DOKill()을 적는걸 주의하기 | |
------- transform.DetachChildren() ------------ | |
자기 자식 오브젝트를 연결 해제한다. 제거하고 나서 이거 한번써주거나 이거 써주고 제거하면 안전하다. | |
------ 포토샵 psb 파일 Resources 폴더에서 동적으로 불러오는 방법 -------- | |
var psbImages = (Resources.LoadAll($"Textures/Cards/{cardRef.slots[i].BaseName}")).ToList(); | |
Sprite _main = nullSprite; | |
Sprite _sub = nullSprite; | |
psbImages.ForEach(obj => { | |
// Debug.Log(obj.name); | |
if(obj is Sprite sprite) { | |
if(sprite.name=="Main") { _main = sprite; } | |
else if(sprite.name=="Sub") { _sub = sprite; } | |
} | |
}); | |
이런식으로 가능 | |
------ List, Transform 같이 IEnumerable 상속받은 class들은 null 연산자 ??=, ?? 안먹음 -------- | |
*GameObject도 안먹는듯 | |
이런거 활용해서 확인하기 | |
1. .FirstOrDefault() ?? | |
2. starList = (!starList.Any()) ? : | |
3. if(costTrans==null) { } | |
4. if(!costTrans) { } | |
------ bounds.Contains 버그 있는듯 --------- | |
2021.3.9f1 기준 boxCollider2D .bounds.Contains(Vector2) 분명히 로그값도 찍어봤는데 인지 못함. 될 때 있다가 안 되는거 보면 확실히 버그 있음. | |
BoxCollider2D.OverlapPoint(point) 2D게임이면 무조건 이거로 체크하는거 추천. | |
------ Mouse 컨트롤 NEW INPUT // OLD INPUT -------- | |
Mouse.current.leftButton.isPressed // equivalent to Input.GetMouseButton(0) | |
Mouse.current.leftButton.wasPressedThisFrame // equivalent to Input.GetMouseButtonDown(0) | |
Mouse.current.position.x // equivalent to Input.mousePosition | |
------- Sprite Renderer에 Raycast하는 방법 / 버튼처럼 사용하고 싶을 때 ------- | |
SpriteRenderer 컴포넌트에 bounds 의 Contains(Vector3)로 마우스 포인트 체크하면 됨. | |
------ C# Dictionary 딕셔너리에서 dic[i]++ 같은 문법 Key 없으면 익셉션 뜸 ------ | |
ContainsKey or TryGetValue 으로 체크해서 처리해야한다. | |
public class NullSafeDict<TKey, TValue> : Dictionary<TKey, TValue> where TValue : class | |
{ | |
public new TValue this[TKey key] | |
{ | |
get | |
{ | |
if (!ContainsKey(key)) | |
return null; | |
else | |
return base[key]; | |
} | |
set | |
{ | |
if (!ContainsKey(key)) | |
Add(key, value); | |
else | |
base[key] = value; | |
} | |
} | |
} | |
이런식으로 Dictionary 수정해서 사용하는것도 | |
------ Input System - Player Input 컴포넌트 넣어준다음 Behavior에서 스크립트로 처리하는 방식 정할 수 있다. | |
using UnityEngine.InputSystem; | |
1. Send Messages, Broadcast Messages - 유니티의 Send Message 기능을 사용하여 특정 함수를 호출하는 방식 | |
특정 키가 들어오면, 특정한 함수를 자동으로 호출하는 방식 | |
Broadcast Messages의 경우, 하위 계층에 있는 오브젝트들까지 제어를 할 수 있다. | |
함수명이 "On + Actions name" 라는게 핵심 | |
예를 들어, "Move"라는 Actions가 있다면, 함수명은 "OnMove" | |
Button으로 설정하면 매개변수 없고 | |
Value인경우 ON됬을때랑 OFF 됬을때 두 번 호출 | |
void OnMove(InputValue value) { ~~ } 매개변수 있는경우 이렇게 | |
2. Invoke Unity Events, Invoke C Sharp Events - 유니티나 C#의 이벤트 기능을 사용하는 방식 | |
Unity의 Event 방식을 사용 | |
해당 Actions에 대한 키 입력이 들어왔을 경우, 설정한 이벤트 함수를 호출해준다. | |
인스펙터에서 달아주기 | |
스크립트 예제 코드 | |
public void OnJump(InputAction.CallbackContext context) | |
{ | |
if(context.performed) // Action Type이 "Button"일 경우 키가 눌렸는지 체크 | |
{ | |
// 점프 로직 | |
} | |
} | |
Button일 경우 | |
_input.leftClick.action.triggered 이런식으로 현재 액션이 performed 되어있는지 체크 할 수 있다. | |
------ GetHashCode 와 GetInstanceID 차이 ------ | |
둘 다 객체 생성하면 같은 값을 가짐. | |
같은 스트링일 경우 같은 값. | |
GetHashCode는 오브젝트에 사용하는 식별 ID - 가벼움 | |
GetInstanceID는 Mono를 상속받은 게임오브젝트에 사용하는 식별 ID - 메인스트림 내에서 관리 살짝 더 무거움 | |
리스트에 컴포넌트/ 오브젝트 같은거 박아두고 체크해서 제거할 때 변수두고 처리하지 않아도 GetHashCode 와 GetInstanceID로 오브젝트나 컴포넌트 ID 확인해서 처리가능 | |
------- 실무에서 Player 클래스에 몰아넣는 방식 VS Static 클래스 VS Singleton 클래스 주관적인 내 의견 ------- | |
일단 Player 클래스에 다 몰아넣고 Singleton 쓰지말라는 건 이해할 수 없음. | |
게임의 형태에 따라 Player 클래스를 싱글톤으로 만들고 몰아넣는게 좋은 경우가 있겠지만 경험상 그렇지 않은 경우가 더 많기에 | |
클래스 설계랑 패턴만 잘 사용해도 싱글톤 때문에 복잡해지고 문제가 된다고 생각하지 않음. | |
정말 필요한 기능들 다 Manager싱글톤 클래스 만들고 접근해 사용하는게 맞다고 생각함. | |
나는 Data관련된 부분만 있는경우 Static 클래스로 구현하고 아니면 싱글톤 클래스 쓴다. | |
ex) DataManager.DEFAULT_DAMAGE_COLOR | |
일단 엔진을 사용하는데 인스펙터를 사용할 수 있다는 장점이 매우 큼. | |
그리고 싱글톤 변수명 instance나 Instance라고 길게 쓰는 경우 많은데 | |
이거 직관성 논할게 아니라 계속 사용하는데 길이를 어느정도 타협해야 한다고 보고 inst라고 적어도 충분히 직관적이기에 난 변수명 Inst로 씀. | |
ex) GameManager.Inst.GamePlay(); | |
------ 경량 패턴 (Flyweight Pattern) -------- | |
인스턴스를 소수만 생성해서 공유하는 패턴 싱글톤과 유사 | |
public class MonsterBlood | |
{ | |
private bool _isMale; | |
private Mesh _skinMesh; | |
private string _bloodName; | |
public bool IsMale { return _isMale; } | |
public Mesh SkinMesh { return _skinMesh; } | |
public string BloodName { return _bloodName; } | |
MonsterBlood(bool isMale, Mesh skinMesh, string bloodName) | |
{ | |
this.isMale = isMale; | |
this.skinMesh = skinMesh; | |
this.bloodName = bloodName; | |
} | |
} | |
public class MonsterBloods | |
{ | |
/* ...생략... */ | |
private static MonsterBlood _ghost = new MonsterBlood(true,ghostMesh,"유령"); | |
private static MonsterBlood _dragon = new MonsterBlood(false,dragonMesh,"드래곤"); | |
private static MonsterBlood _zombie = new MonsterBlood(true,zombieMesh,"좀비"); | |
public static MonsterBlood Ghost{ get{ return _ghost } } | |
public static MonsterBlood Dragon{ get{ return _dragon } } | |
public static MonsterBlood Zombie{ get{ return _zombie } } | |
} | |
public class Monster | |
{ | |
/* ...생략... */ | |
Monster(MonsterBlood blood, MonsterData otherData) | |
{ | |
this._blood = blood; | |
this._monsterData = otherData; | |
//....생략 | |
} | |
private MonsterBlood _blood; | |
public MonsterBlood Blood{ return _blood; } | |
} | |
/** 새로 몬스터를 인스턴스화 할때, 종족과 관련된 정보와 메쉬를 그때 그때 생성활 필요가 없다. **/ | |
Monster newZombie0 = new Monster(MonsterBloods.Zombie,somedatas0); | |
Monster newZombie1 = new Monster(MonsterBloods.Zombie,somedatas1); | |
Monster newZombie2 = new Monster(MonsterBloods.Zombie,somedatas2); | |
Monster newZombie3 = new Monster(MonsterBloods.Zombie,somedatas3); | |
Monster newZombie4 = new Monster(MonsterBloods.Zombie,somedatas4); | |
Monster newZombie5 = new Monster(MonsterBloods.Zombie,somedatas5); | |
Monster newZombie6 = new Monster(MonsterBloods.Zombie,somedatas6); | |
Monster newZombie7 = new Monster(MonsterBloods.Zombie,somedatas7); | |
핵심 : HashMap이나 변수로 new 인스턴스 미리 생성 해두고 static으로 세트 클래스에 접근해서 쓰는 식이다. | |
----- 명령 패턴 (Command Pattern) | |
public interface ICommnad | |
{ | |
void execute(); | |
void undo(); | |
} | |
public class Move: ICommand | |
{ | |
var beforePositionState; | |
void execute() | |
{ | |
beforePositionState = currentPositionState; | |
// some move codes | |
} | |
void undo() | |
{ | |
// some undo move codes to get back to beforePositionState | |
} | |
} | |
public class Draw: ICommand | |
{ | |
var beforeCanvasState; | |
void execute() | |
{ | |
beforeCanvasState = currentCanvasState; | |
// some draw codes | |
} | |
void undo() | |
{ | |
// some undo draw codes to get back to beforeCanvasState | |
} | |
} | |
/* ... 실행부 ... */ | |
Stack<ICommand> commands = new Stack<ICommand>(); | |
public void Execute(ICommand command) | |
{ | |
command.execute(); | |
commands.Add(command); | |
} | |
public void Undo() | |
{ | |
commands.Pop().undo(); | |
} | |
핵심 : 비주얼 노벨, 턴제에서 사용하면 좋음. 명령자체를 받아와서 되돌리기를 할 수 있다는 큰 장점. 스택으로 구현한다는 점 | |
----- 관찰자 패턴 (Observer Pattern) ------ | |
/** 사실 C#은 event와 delegate키워드가 있기 때문에 실제로 C#에선 이보다 매우 간단하다. **/ | |
public enum GameEvent { DEATH,HIT,ATTACK }; | |
public Interface IObserver | |
{ | |
void OnNotify(const Object entity, GameEvent gameEvent); | |
} | |
public class AchievementManger: IObserver | |
{ | |
void OnNotify(const Object entity, GameEvent gameEvent) | |
{ | |
if(gameEvent == GameEvent.DEATH) | |
{ | |
Console.WriteLine(entity.ToString() + "이 죽었습니다! \"넌 뒤져따\" 도전과제 해제!"); | |
} | |
} | |
} | |
public class Subject | |
{ | |
protected List<IObserver> observers = new List<IObserver>(); | |
public void AddObserver(IObserver observer) | |
{ | |
observers.Add(observer); | |
} | |
public void ClearObservers() | |
{ | |
observers.Clear(); | |
} | |
protected void Notify(const Object entity, GameEvent gameEvent) | |
{ | |
foreach(var observer in observers) | |
{ | |
if(observer != null) | |
{ | |
observer.OnNotify(); | |
} | |
else | |
{ | |
observers.Remove(observer); | |
} | |
} | |
} | |
} | |
public class Player: Subject | |
{ | |
/* ...김대기와 같은 적절한 생략을 적절하게... */ | |
private void OnDeath() | |
{ | |
//적절한 죽음과 관련된 코드 | |
OnNotify(this,GameEvent.DEATH); | |
} | |
} | |
/** 런타임에서 **/ | |
myPlayer.AddObserver(achievementManager); | |
/* ... */ | |
if(myPlayer.hp <= 0) | |
{ | |
myPlayer.OnDeath(); | |
} | |
핵심 : 그냥 모든 이벤트를 생각해서 On~~~ Action을 만들고 이벤트가 끝날때 시작할때 그 On~~~~를 실행해준다는걸 항상 인지하고 코딩하면 됨. | |
옵저버를 제대로 쓰는 용도는 도전과제나 퀘스트다 이때는 제대로 클래스를 구현해서 제작하면 됨. | |
------ 프로토타입 패턴 (Prototype Pattern) -------- | |
public class GameCharacter | |
{ | |
public virtual GameCharacter Clone(); | |
} | |
public class Hunter : GameCharacter | |
{ | |
//어떤 코드들 | |
public GameCharacter Clone() | |
{ | |
return new Hunter(attackPoint, defensePoint, speed); | |
} | |
} | |
public class Warrior : GameCharacter | |
{ | |
//어떤 코드들 | |
public GameCharacter Clone() | |
{ | |
return new GameCharacter(attackPoint, defensePoint, speed); | |
} | |
} | |
//새로 객체를 만들고 싶으면 원본 프로토 객체로 부터 복사 생성한다. | |
//원본 | |
GameCharacter prototypicalWarrior = new Warrior(100,20,10); | |
//새로운 객체를 원형 인스턴스로부터 생성. 그리고 이것이 또다른 객체들의 원형이 될수도 있다. | |
GameCharacter prototypicalFastWarrior = prototypicalWarrior.Clone(); | |
prototypicalFastWarrior.speed = 30; | |
// | |
fastWarrior0 = prototypicalFastWarrior.Clone(); | |
fastWarrior1 = prototypicalFastWarrior.Clone(); | |
commonWarrior0 = prototypicalWarrior.Clone(); | |
commonWarrior1 = prototypicalWarrior.Clone(); | |
핵심 : 클래스 제작하다보면 동적으로 깊은 복사가 필요할때 구현해둔다. 크게 중요한건 아닌데 Clone함수를 이해하면 좋음. 특히 new 나 Instanciate 쓰지 말라는 Scriptable Object를 깊은 복사하고싶을때 사용한다. | |
---- 상태 패턴 (FSM : Finite State Machine) ------- | |
Enter(), Update(), Exit() | |
public enum State | |
{ | |
WALK, | |
FLY, | |
FALL, | |
WIN, | |
LOSE, | |
DIE | |
} | |
public State state = State.WALK; | |
private void Update() { | |
switch (state) { | |
case State.WALK: | |
Walk(); | |
break; | |
case State.FLY: | |
Fly(); | |
break; | |
case State.FALL: | |
Fall(); | |
break; | |
case State.WIN: | |
Win(); | |
break; | |
case State.LOSE: | |
Lose(); | |
break; | |
case State.DIE: | |
Die(); | |
break; | |
} | |
} | |
private void ChangeState(State state) { | |
//스테이트에서 나가기전에 마지막으로 실행되는 Exit()함수; | |
switch (state) { | |
case State.WALK: | |
WalkExit(); | |
break; | |
case State.FLY: | |
FlyExit(); | |
break; | |
case State.FALL: | |
FallExit(); | |
break; | |
case State.WIN: | |
WinExit(); | |
break; | |
case State.LOSE: | |
LoseExit(); | |
break; | |
case State.DIE: | |
DieExit(); | |
break; | |
} | |
this.state = state; | |
//스테이트에서 들어가고나서 처음으로 실행되는 Enter()함수; | |
switch (state) { | |
case State.WALK: | |
WalkEnter(); | |
break; | |
case State.FLY: | |
FlyEnter(); | |
break; | |
case State.FALL: | |
FallEnter(); | |
break; | |
case State.WIN: | |
WinEnter(); | |
break; | |
case State.LOSE: | |
LoseEnter(); | |
break; | |
case State.DIE: | |
DieEnter(); | |
break; | |
} | |
} | |
private void WalkEnter() { | |
//진입시 속성을 설정합니다 | |
} | |
private void Walk() { | |
//업데이트문에서 이동을 시킵니다 | |
transform.position += currentDir * dirNo * speed * Time.deltaTime; | |
} | |
private void WalkTrigger(Collider other) { | |
//OnTrigger도 스위치로 쪼개서 상태별로 작동방식을 다르게 달아줄수있습니다 | |
} | |
private void WalkTriggerStay(Collider other) { | |
//추가적으로 OnCollision, TrigerStay등도 스위치와 함수를 추가해주시면 깔끔하게 관리가능 | |
} | |
private void WalkExit() { | |
//상태 종료시 속성을 설정합니다 | |
} | |
핵심 : enum 형태로 state 변수를 만들고 state에 들어갈때 도중에 나갈때를 확인해서 처리하는 방식. | |
코드 설계 자체를 상태의 변화로 설계하면 이해하기 쉽다. | |
ps. 쓸대없는 얘기지만 FSM은 옛날 게임에서 많이 볼 수 있다. 일본 게임 개발자들이 많이 쓰는 방식이였다. | |
------ rigidbody is Kinematic 이 켜져있으면 collider raycast 안먹는다. -------- | |
------- i2 Localization 사용법 ---------- | |
파라미터 동적으로 추가하려면 아래 내용 적어줘야함. | |
1. localDescParam._Params = new List<LocalizationParamsManager.ParamValue>(); | |
Localize.SetTerm (기본 사용법) | |
2. localDescText.SetTerm($"CARD/{cardRef.baseName[(int)cardRef.cardLevel]}/DESC"); | |
데이터(string) 그대로 가져오는 거 | |
3. var oriText = LocalizationManager.GetTranslation(localDescText.mTerm); | |
파라미터 동적으로 집어넣기 SetValue 하는 예시 | |
4. localDescParam.SetParameterValue("D_COLOR","#"+ColorUtility.ToHtmlStringRGB(color)); | |
5. 시트에서 찾아보고 없는경우 다른 Term 설정 | |
if(LocalizationManager.TryGetTranslation($"Task/{task_.slug}/StatusMessage",out resultText)) { | |
taskInfo.GetChild(2).GetComponent<Localize>().SetTerm($"Task/{task_.slug}/StatusMessage"); | |
} else { | |
taskInfo.GetChild(2).GetComponent<Localize>().SetTerm($"Task/{task_.slug}/Name"); | |
} | |
6. 카테고리로 Sheet에 있는 모든 칼럼을 가져오기 랜덤 텍스트 보여줄때 사용 가능 (7개를 가져오고 싶을때) | |
LocalizationManager.GetTermsList("UI/LOADING"); | |
UI 탭아래 이렇게 있을 경우 | |
LOADING/PHRASE_00 Clearing the Game Board... | |
LOADING/PHRASE_01 Repairing Broken Blocks... | |
LOADING/PHRASE_02 Blocks Stretching... | |
LOADING/PHRASE_03 Aligning the Pieces... | |
LOADING/PHRASE_04 Chasing Escaped Blocks.. | |
LOADING/PHRASE_05 Preparing an Interesting Ad... | |
LOADING/PHRASE_06 Removing Stuck Blocks... | |
** 잘 모르는 꿀팁 Key의 구조는 탭/키 이런식으로만 가능한게 아니다. | |
한 마디로 시트의 UI 탭에 LOADING_01 이렇게만 키를 지정할 수 있는게 아니라 UI 탭에 LOADING 에 A01 이런식의 더 깊은 구조도 가능. | |
항상 "탭이름/Key의 이름/Key의 추가 카테고리/계속... 가능" | |
"탭이름/"+ "A/B/.." 이런식으로 구성가능 | |
------ 타원형 표현식 ------- | |
x = centerX + (semi-major * sin T) | |
y = centerY + (semi-minor * cos T) | |
float alpha = 0f; | |
void Update () | |
{ | |
//transform.position = new Vector2(center.x + (semiMajor * Mathf.Sin(AngleX)), | |
// center.y + (semiMinor * Mathf.Cos(AngleY))); | |
transform.position = new Vector2(0f + (10f * Mathf.Sin(Mathf.Deg2Rad * alpha)), | |
0f + ( 5f * Mathf.Cos(Mathf.Deg2Rad * alpha))); | |
alpha += 5f;//can be used as speed | |
} | |
x = centerX + {semi-major * cos(alpha)*cos(tiltAngle) - semi-major * sin(alpha)*sin(tiltAngle)} | |
y = centerX + {semi-minor * cos(alpha)*sin(tiltAngle) + semi-minor * sin(alpha)*cos(tiltAngle)} | |
public float alpha = 0f; | |
public float tilt = 45f; | |
void Update () | |
{ | |
transform.position = new Vector2(0f + (10f * MCos(alpha) * MCos(tilt)) - ( 5f * MSin(alpha) * MSin(tilt)), | |
0f + (10f * MCos(alpha) * MSin(tilt)) + ( 5f * MSin(alpha) * MCos(tilt))); | |
alpha += 5f; | |
} | |
float MCos(float value) | |
{ | |
return Mathf.Cos(Mathf.Deg2Rad * value); | |
} | |
float MSin(float value) | |
{ | |
return Mathf.Sin(Mathf.Deg2Rad * value); | |
} | |
------ DOTween에서 Time 값을 무시하려면 Time이 0인데 DOTween 은 작동시키고 싶을때 ------ | |
끝에 .SetUpdate(true); 붙이면 됨! | |
ex) uiKeyPanel.keyRect.DOLocalMoveY(-12f, 0.2f).SetLoops(-1, LoopType.Yoyo).SetUpdate(true); | |
------ Collider.bounds.ClosetPoint 말고 Collider.ClosestPoint() 쓰면 Box형태말고도 다른 콜라이더에서 가까운 포인트를 잡을 수 있다. ------- | |
------ 2D 인지하는 Physics2D.OverlapPoint(Vector2, LayerMask) 에서 트리거는 인식 못한다. ------ | |
----- uGUI /// <summary> UI에 레이캐스트하여 가장 위에 있는 대상 가져오기 </summary> | |
private GraphicRaycaster gr; | |
private PointerEventData ped; | |
private List<RaycastResult> rrList; | |
private void Awake() | |
{ | |
gr = GetComponentInParent<GraphicRaycaster>(); | |
ped = new PointerEventData(EventSystem.current); | |
rrList = new List<RaycastResult>(4); | |
} | |
private void Update() | |
{ | |
ped.position = Input.mousePosition; | |
} | |
private T RaycastUI<T>() where T : Component | |
{ | |
rrList.Clear(); | |
gr.Raycast(ped, rrList); | |
if (rrList.Count == 0) | |
return null; | |
return rrList[0].gameObject.GetComponent<T>(); | |
} | |
// UGUI Canvas의 Render Mode가 'Screen Space - Overlay' 일 경우 | |
// GraphicRaycaster 컴포넌트를 통해 마우스 이벤트를 받는 예시 | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.EventSystems; | |
using UnityEngine.UI; | |
public class GraphicRaycasterEx : MonoBehaviour | |
{ | |
// 예시에서 사용할 GraphicRaycaster객체 | |
private GraphicRaycaster gr; | |
private void Awake() | |
{ | |
gr = GetComponent<GraphicRaycaster>(); | |
} | |
private void Update() | |
{ | |
var ped = new PointerEventData(null); | |
ped.position = Input.mousePosition; | |
List<RaycastResult> results = new List<RaycastResult>(); | |
gr.Raycast(ped, results); | |
if (results.Count <= 0) return; | |
// 이벤트 처리부분 | |
results[0].gameObject.transform.position = ped.position; | |
} | |
} | |
------ uGUI / Image 컴포넌트에서 투명한 부분 raycast 안되게 하는방법 ------- | |
아래 코드를 적용시키고 Image Source 설정을 Mesh Type - Full Rect, Read/Write Enabled - Check 해주면 됨. | |
imgButton = GetComponent<Image>(); | |
imgButton.alphaHitTestMinimumThreshold = 0.5f; // alpha값 50% 이상만 체크 더 올리거나 낮춰도 상관없음. | |
------- Script로 Particle System 파티클 다루기 ------ | |
startColor와 같은 일반 내용은 main 모듈 | |
아래와 같이 모듈을 받아와서 수정하면 됨. | |
var ma = stars.main; | |
ma.startColor = Color.red; | |
var sh = stars.shape; | |
sh.scale = new Vector3(2f, 1f, 1f); | |
------- Unity Gradient Color 를 스크립트로 다루기 --------- | |
Gradient클래스에는 GradientAlphaKey(alphaValue, alphaTime) 와 GradientColorKey(Color, ColorPos) 가 있는데 | |
AlphaKey가 위에 체크포인트, ColorKey가 아래 포인트임 | |
이런식으로 변수 선언 가능 | |
private Gradient A = new Gradient { | |
alphaKeys = new[] { new GradientAlphaKey(0, 0f), new GradientAlphaKey(1, 1f) }, | |
colorKeys= new[] { new GradientColorKey(Color.red, 0f), new GradientColorKey(Color.black, 0f), new GradientColorKey(Color.blue, 0f)} | |
}; | |
------ 만약 URP 2D 프로젝트인데 Covnert했는데도 안나올경우 "Use SoftParticle Factor?" 끄기 ----- | |
2D or 2D Experimental and you can't see the effect, please set the "Use SoftParticle Factor?" bool parameter of all materials to off | |
------- OnPointerDown에서 왼쪽 클릭만 체크하는 법 ------- | |
public void OnPointerDown(PointerEventData eventData) { | |
if(eventData.button == PointerEventData.InputButton.Left) { | |
이런식으로 가능 | |
} | |
} | |
------- MasterAduio Asset 사용법 ------- | |
using DarkTonic.MasterAudio; | |
전체음향 | |
PersistentAudioSettings.MixerVolume.Value 효과음 용 | |
settingPanel.soundAllSlider.value = Mathf.RoundToInt(PersistentAudioSettings.MixerVolume.Value * 100); | |
MasterAudio.PlaylistMasterVolume = (value * 0.01f); 전체 BGM 끄기 | |
두개 다 처리해 줘야함 | |
SFX (audio clip volume) X (group volume) X (bus volume - if any) X (master mixer volume) | |
BGM (audio clip volume) X (playlist volume) X (master playlist volume). | |
BGM 처리 | |
MasterAudio.TriggerPlaylistClip("BGM","bgm_stage1"); | |
MasterAudio.StopPlaylist("BGM"); | |
PlaylistController.InstanceByName("BGM").PlaylistVolume = (value * 0.01f); BGM의 특정 Playlist 끄기 | |
SFX 처리 | |
MasterAudio.PlaySound("fx_heal"); | |
MasterAudio.GrabBusByName("SFX").volume = (value * 0.01f); | |
if(userData.SFXVolume >= 1.0f) { | |
MasterAudio.UnmuteBus("SFX"); | |
} else { | |
MasterAudio.MuteBus("SFX"); | |
} | |
if(userData.BGMVolume >= 1.0f) { | |
MasterAudio.UnmuteBus("BGM"); | |
} else { | |
MasterAudio.MuteBus("BGM"); | |
} | |
버스로 뮤트하는 방법 | |
------ NEW INPUT SYSTEM 설명 ------ | |
일단 Default Assets의 ActionMap들을 복사해서 세팅하는게 좋다. | |
Input Action Asset 만든다음 Create 누르면 C# 파일 생성되는데 | |
예를들어 CardGameInput 이라는 이름으로 만들었다면 | |
public CardGameInput inputSystem; | |
void Awake() { | |
inputSystem = new CardGameInput(); | |
inputSystem.UI.Setting.performed += (value)=> { | |
ToggleSettingUI(); | |
}; | |
inputSystem.Enable(); // 이걸 해줘야 코드 바꾼거로 적용됨 | |
} | |
이런식으로 내가만든 InputActionAssets를 변수로 만들고 UI라는 InputMap에 내가만든 Setting이라는 InputAction이 작동 됬을때 행동을 넣어 코드를 짤 수 있다. | |
------ Input Action Event에는 총 3가지 behavior가 있다. | |
started, performed, canceled | |
완전히 실행될때만 진행하고 싶으면 이런식으로 처리 | |
public void SomeAction(ActionState keyState) | |
{ | |
if (!IsValidAction()) return; | |
switch (keyState) | |
{ | |
case ActionState.Started: | |
_isOnAction = true; | |
break; | |
case ActionState.Performed: | |
CompleteAction(true); | |
break; | |
case ActionState.Canceled: | |
if (!_isOnAction) return; | |
CompleteAction(false); | |
break; | |
} | |
} | |
이런식으로 맵을 교체가능 | |
_player_input.SwitchCurrentActionMap(actionMapName); | |
------ Input.mousePosition 이런식의 코드는 이제 그만 ------- | |
NewInputSystem이후로는 Input.~~ 블라블라 쓰지말고 Mouse.current.~~ 코드를 | |
ex) Input.mousePosition 대신 Mouse.current.position.ReadValue() | |
OnMouse~~~코드 안먹는다 이제 따라서 IPointer랑 카메라에 Physics Raycast 붙여서 진행해야함 | |
----- Input Action Disable로 끄고 킬수있다. Map에 있는 것도 가능. 단 맵 바꾸면 다 켜지는 듯 -------- | |
ex) public InputAction sprintAction; | |
_playerInput = GetComponent<PlayerInput>(); | |
sprintAction = _playerInput.actions.FindActionMap("Player")["Sprint"]; | |
sprintAction.Disable(); | |
Player Map에 Sprint Action을 Disable 시킨다. | |
----- Input Action를 사용해야하는 이유 ------- | |
1. 플랫폼 간 제어를 쉽게 만든다. | |
- 키보드, Xbox 컨트롤러, PlayStation 게임패드 등을 통해 동일한 작업을 수행하도록 개발하는 것이 더 수월 | |
- 모든 장치를 일관되고 동기화된 상태로 유지 | |
2. 편집기를 통해 Action Maps, Actions 등을 쉽게 편집할 수 있다. | |
- 또한 현재 사용되는 입력들에 대해 한 눈에 파악할 수 있다. | |
3. 이벤트로 작동되기 때문에 Update()에서 계속 확인하지 않아도 되어 이산 입력에 최적화된다. | |
- 단 스틱과 같은 연속 입력에는 폴링과 정기적인 업데이트가 여전히 필요 | |
4. 런타임 중에 현재 활성화된 입력을 빠르게 확인할 수 있는 입력 디버거가 내장되어 있다. | |
- 추가 샘플을 설치하면 플레이 하는 동안 화면에 게임패드/기본입력을 직접 시각화 가능 | |
New Input System 설치하려면 Package Manager에서 Input System > Install 하면 된다. | |
------ Git Unity Smart Merge ---- | |
협업할 때 유니티에서 제공하는 스마트 머지 설정하면 자동으로 합쳐져서 편함 | |
문서 검색하면 나옴 | |
그냥 .gitignore에 사용하겠다는 여부랑 +스마트 머지 파일 경로 적어주면 됨. | |
----- Timeline 부분 ------ | |
Playable Director 와 Timeline Asset 스크립트 접근 | |
director.playableAsset = newTimelineAsset; | |
timeline = director.playableAsset as TimelineAsset; | |
// rebuild for runtime playing | |
director.RebuildGraph(); | |
director.time = 0.0; | |
director.Play(); | |
// 트랙 내용 바꾸기 | |
director.SetGenericBinding(track, bindings); | |
ex) | |
if(loadedSceneName=="S01Zoo") { | |
playableDirector = GameObject.Find("TIMELINE/Intro").GetComponent<PlayableDirector>(); | |
nowTimelineAsset = GameObject.Find("TIMELINE/Intro").GetComponent<PlayableDirector>().playableAsset as TimelineAsset; | |
playableDirector.SetGenericBinding(nowTimelineAsset.GetOutputTrack(3), UIManager.Inst.fadePanel); | |
playableDirector.SetGenericBinding(nowTimelineAsset.GetOutputTrack(4), GameObject.FindGameObjectWithTag("GameManager").GetComponent<SignalReceiver>()); | |
playableDirector.Play(); | |
} | |
----- 현재 보고 있는 Scene 의 Transform 값으로 카메라 동기화 시킬때 쓰는 단축 키 Ctrl+Shift+F ------- | |
Align With View | |
------ Unity Cursor 커서 보이고 안보이고 설정하기 ----- | |
using UnityEngine; | |
Cursor.visible = false; | |
Cursor.lockState = CursorLockMode.Locked; | |
------ 프로퍼티 인스펙터에서 보이게 하는 Attribute -------- | |
[field: SerializeField] 를 사용하면 프로퍼티도 보이게 가능 | |
------ 2D PSD Importer 2022.2 부터 설정에서 여백 적용가능 ------ | |
------ TMProWarpText 기능있음------ | |
텍스트 돌리는 애니메이션 꺽는 스크립트 가능 TMP로 | |
------ Dissolve Shader Effect ---- | |
사라지는 이펙트 많이쓰는 쉐이더 | |
------ 방향 같을때 0, 내적 cos 만 생각하면 됨 ------ | |
내적 은근 쓸일 많음 | |
------- fixedDeltaTime은 1초에 50번 실행되므로 TimeScale에 0.02를 곱해주면 한 프레임에 해당되는 시간이 된다. ------ | |
------- uGUI Layout Group Smooth 애니메이션 적용시키는 에셋이나 코드 -------- | |
Asset - Smooth Grid Layout UI - Documentation | |
Code - https://gist.github.com/codorizzi/79aab1ae7d7940fe3e3603af61cd8617 | |
둘 다 다른거 하나는 DoTween으로 만든거 같음 | |
------ overlap 이라는 일정 범위에 충돌체를 감지하는 기능도 있다. ---------- | |
Physics2D.OverlapPointAll 이런식으로 사용 | |
Physics 함수에 들어가있음. | |
------ uGui Grid나 LayoutGroup에서 요소 Child 값 안바꾸고 레이어링 바꾸고싶을때 ----- | |
Canvas 컴포넌트 붙여서. Sorting Layer 랑 Order in Layer 바꾸면 됨. | |
------ TMP Font Asset Creator 한글 폰트 세팅 ------ | |
Padding - 4 | |
Resolution - 4096*4096 | |
Sampling Point Size - 32 | |
Character Set - Unicode Range (Hex) | |
0A,0D,20-7E,2010,2013-2016,2018-2019,201B-201D,201F-2022,2025-2026,2030,2032-2036,2039-203C,203E,2042,2074,207A-207F,2081-2084,20A9,2103,2109,2113,2116,2121-2122,2126,212B,2153-2154,215B-215E,2160-2169,2170-2179,2190-2199,21B0-21B4,21BC,21C0,21C4-21C5,21CD,21CF-21D4,21E0-21E3,21E6-21E9,2200,2202-2203,2206-2209,220B-220C,220F,2211,2213,221A,221D-2220,2222,2225-222C,222E,2234-2237,223C-223D,2243,2245,2248,2250-2253,225A,2260-2262,2264-2267,226A-226B,226E-2273,2276-2277,2279-227B,2280-2287,228A-228B,2295-2297,2299,22A3-22A5,22BB-22BC,22CE-22CF,22DA-22DB,22EE-22EF,2306,2312,2314,2460-2487,249C-24E9,24EB-24F4,2500-2503,250C-254B,2592,25A0-25A1,25A3-25A9,25B1-25B3,25B5-25B7,25B9,25BC-25BD,25BF-25C1,25C3,25C6-25CC,25CE-25D1,25E6,25EF,2605-2606,260E-260F,261C-261F,262F,2640,2642,2660-2661,2663-2665,2667-266A,266C-266D,266F,3131-3163,AC00-D7A3 | |
* 주의 : Character Set 할때 공백 있으면 안됨! | |
------ c# 생성자 간략표현 ------- | |
public class A { | |
public string a; | |
public int b; | |
} | |
var aInst = new A() { a = "여기에 넣기", b = 3 }; | |
------Ignore Raycast 설정 --------- | |
OnMouse~~ 함수 Trigger 무시시킬려면 Layer를 2 Ignore Raycast로 설정하면된다. | |
------ Unlit과 Lit의 차이 -------- | |
Unlit은 빛의 영향을 안받는 것 | |
Lit은 빛의 영향을 받는 쉐이더를 뜻함. | |
------ URP로 프로젝트 바꾸는 방법 --------- | |
PackageManager에서 URP 추가하고 | |
Project에서 URP Asset 추가한 후 | |
Build > Graphic > Asset 등록 아래에 URP Grobal Settings 세팅되었는지 체크 | |
Build > Player > OtherSettings 에서 Rendering - Gamma에서 Linear로 바꾸기 (기본 세팅임) | |
------ 인스펙터에서 클래스나 컴포넌트 연결하는 부분에 ctrl+c ctrl+v 나 키입력하면 빠르게 세팅이 가능하다 -------- | |
------- 3D starter assets ---------- | |
이걸로 유니티에서 제공하는 1인칭 3인칭 컨트롤러 부분 다 구현하는게 편하고 빠르다. | |
------ 분홍색으로 깨질 때 URP 파이프라인 수정하는 거 작업해주면 됨 ------- | |
기존 쉐이더를 URP 쉐이더로 바꿔주는 작업을 하면 됨. | |
폴더 아무거나 클릭한다음 Window > Renderer > Render Pipeline Converter 킨다음 Bulit-in to URP 체크박스 다하고 Initialize Converters 하고 Covnert Assets하면 됨. | |
-------- 2D 이면 2D 3D면 3D로 값 넣어주기---------- | |
inven.sackArea.bounds.Contains((Vector2)transform.position); YES //Collider2D일 경우 | |
inven.sackArea.bounds.Contains(transform.position); NO! | |
------ Color To Hex 로 바꿔야할때 Color클래스 -> FFFFFF스트링 ------ | |
ColorUtility.ToHtmlStringRGB( myColor ) | |
------ Layout Group Update 바로 안되서 어긋날 때 -------- | |
정렬 제대로 안 될때, | |
horizLayoutGroup.CalculateLayoutInputHorizontal(); | |
horizLayoutGroup.CalculateLayoutInputVertical(); | |
horizLayoutGroup.SetLayoutHorizontal(); | |
horizLayoutGroup.SetLayoutVertical(); | |
or | |
messageContainer.GetComponent<VerticalLayoutGroup>().enabled = false; | |
messageContainer.GetComponent<VerticalLayoutGroup>().enabled = true; | |
or | |
private void LateUpdate() { | |
LayoutRebuilder.ForceRebuildLayoutImmediate(comboGuideRectTrans); | |
LayoutRebuilder.ForceRebuildLayoutImmediate(comboStackRectTrans); | |
} | |
이런식으로 처리해야함 | |
------ 정리 안하는 나지만 정리하는 습관 있는 분들에게 유용한 팁 ------ | |
주석 | |
#region UNITY_EVENTS | |
start() { ~~ } | |
awake() { ~~ } | |
#endregion | |
이런식으로 만들어두면 한번에 접어둘 수 있어서 코드 많아질 때 스크롤 덜 내릴수있음. | |
복붙용 내가만든 region | |
#region VARIABLES & PROPERTIES | |
#endregion | |
#region UNITY_EVENTS | |
#endregion | |
#region MAIN_FUNCTIONS | |
#endregion | |
#region SUB_FUNCTIONS | |
#endregion | |
#region UI_FUNCTIONS | |
#endregion | |
#region COROUTINE_FUNCTIONS | |
#endregion | |
-------- LocalPosition 을 WorldPosition으로 변경하는 코드 ----------- | |
Vector3 lPos; // your local pos | |
Vector3 wPos = transform.TransformPoint(lPos); | |
------- trigger raycast 끄는거 Project Settings > Physics 2D or Physics > Queries Hit Triggers ------ | |
-------- OnMouse시리즈 2d Collider 작동 안할때 --------- | |
카메라에 Physics2DRaycaster 달면 됨. | |
-------- 흑흑.. 하루종일 깨지고 나서 2D에 대한 총 정리 ---------- | |
1. uGUI | |
장점 - ui코드를 내가 일일히 직접 작성하지 않아도 됨 | |
해상도 및 화면 비율에 따른 배치가 겁나 쉬워짐 | |
2. SpriteRenderer | |
장점 - 이동이 들어갈 경우 uGUI다 효율이 좋다.. | |
물리 관련된 처리가 쉽다. | |
(단, 해상도 처리도 직접해야함..) | |
구현 부분을 1번으로 만들려다가 포기하는 가장 큰 이유가 collider문제 아닐까 OnMouse시리즈는 콜리더 여부에 따라 적용되기에 | |
2번으로 눈물 닦으면서 선회한다... | |
> 최근 RaycastCollider라는 방안이 나왔다는데 체크해보길.. 하지만 uGUI에게 화가난 나는 도저히 쓸 수 없다.. | |
2번하고 Text만 1번을 섞어쓰는 경우가 있을 수 있는데 이러다 Mask 문제에 직면하면 문제가 생길 수 있다. | |
TMP_Text 말고 TMP_MeshText 로 uGUI에서 벗어나자 | |
Mask 할때 Material Stencil Comp값을 3 으로 바꾸는 방법 참고 | |
사실 상 대부분의 단순한 ui기획의 게임이 아니라면, 프로토타입의 수준의 게임이 아닌 이상 | |
2D게임은 이동이 들어가는 경우가 대부분이기에 2번을 선택할 수 밖에없다. | |
섞어쓰다가 결국 2번으로 몰빵하게 되었다. | |
1번은 말그대로 진짜 UI에만 적용하는게 맞는듯.. 고정 되어있고 해상도에 따라 조금씩 배치나 크기가 달라져야하는 물리가 필요없는 것들.. | |
2번 SpriteRenderer로 대부분의 ui를 구현하려하다보니 머리가 깨질거 같다. | |
2번 사용시 항상 걸릴 수 밖에없는 문제 프리팹식으로 쓰기에 OrderLayer순서가 겹치는데 | |
SortingGroup 이라는 컴포넌트를 사용하면 OrderLayer를 코드로 정해주는 뻘짓 할 필요없다. (ex) _index라는 int 변수 * 30(한 프리팹에 모든 레이어 수보다 큰 대략의 수) 이런식으로 OrderLayer 바꿔주는 미친짓) | |
SpriteMask 안에 SpriteMask가 있는 경우 이중 마스크일 경우 SortingGroup으로 처리를 하라는데 참고 | |
다시 돌아보며.. 1, 2 둘다 사용하는것도 낫 베드 | |
AObject | |
AUIObject | |
이런식으로 프리팹 두개 만들어두고 참조 | |
------ sprite에서 오른쪽 클릭 체크 하고 싶을때 ------ | |
function OnMouseOver () { | |
If(Input.GetMouseButtonDown(1)){ | |
//do stuff here | |
} | |
} | |
------- UnityEngine.Pool 오브젝트 풀을 사용할 수 있다. --------- | |
------- C# unity JSON Paser는 Asset Store에 있는 JSON .Net For Unity 에셋이 최고다. Netsoft 꺼 ㅎㅎ ----- | |
-------- 특정언어에서 Iterator, IEnumerator 를 사용이 필요한 순간은 반복문 도중 삭제하거나 추가하거나 이럴 때 필요하다 ------ | |
c#은 도중에 삭제시 리버스 포문이나 toList를 통해서 기존 리스트를 기준으로 반복문 돌리든 (단, 추가될 경우에는 얘기가 다름.) | |
------- Shader Build시에만 안보였던 문제 해결 ------ | |
All in 1 Shader 에셋에서 EnableKeyword / DisableKeyword 로 반복적으로 키고 끄는 경우 빌드시 shader가 깨질 수 있었던 버그가 있었다. | |
그럴때는 걍 켜놓고 SetFloat, SetColor 이런걸로 값을 끈것처럼 표현해서 처리해야한다. | |
-------- ?? 널 병합연산자 사용시 ------ | |
A ?? B 이렇게 띄어쓰기를 해야 적용됨 !! | |
-------- instanceof super 클래스 종류 체크하는 방법 ------ | |
target.GetType()==typeof(SuperClass) | |
typeof(EliteRoom).IsInstanceOfType(currRoom) 같은거 | |
------ OnMouseEnter vs OnPointerEnter 차이점 ------ | |
Collider 영향받는게 Mouse, Pointer는 안받음 | |
UGUI는 Pointer, SpriteRenderer나 Gameobject일경우 Mouse 쓰면 됨 | |
OnPointer~ 개빡치는게 간혹 투명한 부분도 인식함. | |
------ 동적으로 ugui 사이즈 바로 안바뀔때 ------ | |
1. LayoutRebuilder.ForceRebuildLayoutImmediate(rectTrans) 해주면 됨. | |
2. Content Size Fitter와 SetActive로 키고 끄고 후 size 변화주면 프레임마다 적용됨. | |
------- Runtime에 For문 안에 collection 값 수정할 경우 --------- | |
이 방법이 짱 | |
foreach (var item in list.ToList()) { | |
list.Remove(item); | |
} | |
Scenario 1 – The collection is modified in the foreach loop | |
``` | |
foreach (var movie in movieCollection) | |
{ | |
if (movie.Contains(removeMovie)) | |
{ | |
movieCollection.Remove(removeMovie); | |
} | |
} | |
``` | |
Solution 1 – If you’re removing items, use RemoveAll() | |
``` movieCollection.RemoveAll(movie => movie.Contains(removeMovie)); ``` | |
Solution 2 – If you’re adding items, put them in a temp and use AddRange() | |
``` | |
var itemsToAdd = new List<string>(); | |
foreach (var movie in movieCollection) | |
{ | |
if (movie.Contains(duplicateMovie)) | |
{ | |
itemsToAdd.Add(duplicateMovie); | |
} | |
} | |
movieCollection.AddRange(itemsToAdd); | |
``` | |
Solution 3 – Use a regular for loop and loop in reverse | |
``` | |
for (int i = movieCollection.Count - 1; i >= 0; i--) | |
{ | |
if (movieCollection[i].Contains(duplicateMovie)) | |
{ | |
movieCollection.Add(duplicateMovie); | |
} | |
} | |
``` | |
------------ IEnumerator 변수 선언해서 집어넣은 IEnumerator 함수 Coroutine으로 왜 두번째 실행했을 때 실행이 안되는지? -------------- | |
질문 | |
------------- Switch Case 에서 만약 지역변수를 넣을거면 중괄호를 사용해야한다. ------------- | |
문에서 { } 를 사용하면 지역변수의 범위를 한정지을 수 있다. | |
ex) | |
switch(key) { | |
case "1": | |
var a="1"; | |
break; | |
case "2": | |
var a="2"; | |
break; | |
} | |
이렇게 하면 에러뜸 | |
switch(key) { | |
case "1": | |
{ | |
var a="1"; | |
break; | |
} | |
case "2": | |
{ | |
var a="2"; | |
break; | |
} | |
} | |
이런식으로 중괄호로 구분지어줘야함. | |
------------ Monobehavior 상속 받은 오브젝트 참조시 null 나오는 이유 -------------- | |
new 로 생성해서 그렇다. 절대 new로 생성하면 안 된다. | |
-------------- IPointer~~Handler 실행 안될때 체크해야할 것들 ---------- | |
1. Added EventSystem game object to scene (Create -> UI -> Event System) | |
2. Camera has a Physics Raycaster (Select Main Camera, Add Component -> Event -> Physics Raycaster) | |
3. Selectable object is a MonoBehavior-derived class that implements IPointerClickHandler, IPointerDownHandler, and IPointerUpHandler (see accepted answer). | |
4. Selectable game object includes selectable object MonoBehavior script. | |
5. Selectable game object includes a collider (box, mesh, or any other collider type). | |
6. Check Raycaster Event Mask vs. game object's Layer mask | |
7. Verify no collider (possibly without a mesh) is obscuring the selectable game object. | |
8. If collider is a trigger, verify that Queries Hit Triggers is enabled in Physics settings. | |
------------ 절차지향 C# 에서 자바스크립트같은 비동기 처리 UniRx ---------------- | |
시간 관련 코드나 Update안에 적어야하는 코드 (요청하고 기다리는 코드)는 UniRx를 사용하면 성능과 작업 효율성이 매우 좋다. | |
---------- 실무 ----------- | |
DOTween 가장 많이 쓰고 | |
오디오는 구현해서 쓰는경우가 많은듯 | |
--------- JSON 처리 --------- | |
실무에서는 JSONUtiltiy 안쓰고 .Net Json Asset 쓰는편 | |
네트워킹에서 개인적으로 받은 JSONObject 프레임워크도 좋았던거 같음(git조사필요) | |
---------- abstract class는 유니티에서 인스턴스를 생성할 수 없는데 mono상속한 클래스 구현한 자식을 생성해서 Debug.log 로 abstact class 를 찍으면 null이 찍히는 문제가 있어 데이터를 파악하기 힘들다. ------- | |
다면 형채는 존재하므로 . 으로 접근해서 멤버변수를 쓸수있고 제대로 나온다. | |
따라서 mono를 빼고 객체지향 클래스 설계로 가든지, mono 붙이고 프리팹 상속을 사용하든지 둘 중 하나를 선택하는 식으로 설계해야한다. | |
----------- UnityEvent vs System.event --------- | |
C# Event가 더 좋으니 | |
UnityEngine.Events UnityEvent 보다는 | |
delegate와 함께 System event를 사용하자 | |
-------- raycast 예제 ------------ | |
Debug.DrawRay로 레이를 디버거로 그려볼수있다. | |
public void OnDrag(PointerEventData eventData) | |
{ | |
var _targetPoint = Camera.main.ScreenToWorldPoint(new Vector3(eventData.position.x, eventData.position.y, 10f)); | |
targetPoint = new Vector3(_targetPoint.x,_targetPoint.y,targetPoint.z); | |
RaycastHit hit; | |
Debug.DrawRay(_targetPoint, Vector3.back, Color.red, 3.0f); | |
if(Physics.Raycast(_targetPoint, Vector3.back, out hit, 20.0f)) { | |
Debug.Log(hit.collider.gameObject.name); | |
if(hit.collider.gameObject.tag=="Enemy") { | |
Debug.Log("HIT!"); | |
} | |
} | |
} | |
-------- 클래스를 parameter로 넘길때 쓰는 방법은 typeof(클래스) ------------ | |
------- Shuffle 코드 -------- | |
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Int32? seed = null) { | |
List<T> buffer = source.ToList(); | |
Random random = seed.HasValue ? new Random(seed.Value) : new Random(); | |
Int32 count = buffer.Count; | |
for (Int32 i = 0; i < count; i++) { | |
Int32 j = random.Next(i, count); | |
yield return buffer[j]; | |
buffer[j] = buffer[i]; | |
} | |
} | |
-------- Abstract 클래스 상속받은 클래스를 매개변수로 받고 생성하는 코드 --------- | |
public static void AssignRowNodeAsRoom(List<MapNode> _row, Type _class) { | |
_row.ForEach(node => { | |
if(node.GetRoom() == null) { | |
try { | |
node.SetRoom(Activator.CreateInstance(_class) as BaseRoom); | |
} catch { | |
} | |
} | |
}); | |
} | |
이런식으로 표현 가능 Type으로 클래스받고, Activcator.CreateInstance(상속받은클래스)로 생성가능 | |
-------- 특정 오브젝트만 보는 단축키 개 유용하다 Shift + H ------------- | |
--------- event 는 delegate를 가공한 느낌이라고 보면 된다 -------- | |
씬 로드시 변수 동적할당에 유용하게 쓸 수 있다. | |
Await에서 씬변경시 초기화할 Init 함수들을 매너저에 event나 delegate를 만들어 넣으면 된다. | |
선언방식 | |
event 델리게이트핸들러 이름; | |
delgate 리턴타입 델리게이트핸들러==변수명(매개변수 타입); | |
--------- sceneLoaded 이벤트 실행 순서 ---------------- | |
Awake -> OnEnable -> SceneManager.sceneLoaded -> Start | |
--------- 특정 게임 오브젝트 x의 전체경로 가져오는 방법 ---------- | |
string _path = string.Join("/", x.GetComponentsInParent<Transform>().Select(t => t.name).Reverse().ToArray()); | |
------ JPG 불러올때 ----- | |
``` | |
JPG 파일을 읽으면 RGB24로 기본 포멧이 변경된다... | |
PNG는 RGBA32로 덕분에 JPG로 로딩된 텍스쳐는 재 변환을 거쳐야 한다. | |
Texture2D tx; | |
tx.LoadImage(jpg data bytes~); | |
Color[] txColor = tx.GetPixels(); | |
tx.Resize(tx.width, tx.height, TextureFormat.RGBA32, false); | |
tx.SetPixels(txColor); | |
tx.Apply(); | |
``` | |
------ Texture2D Resize -------- | |
유니티에서 Texture 사이즈를 변경하고싶으면 Resize함수가 사라졌기 때문에 직접 구현해야한다. | |
예제 | |
``` | |
public Texture2D ScaleTexture(Texture2D source, float _scaleFactor) | |
{ | |
if (_scaleFactor == 1f) | |
{ | |
return source; | |
} | |
else if (_scaleFactor == 0f) | |
{ | |
return Texture2D.normalTexture; | |
} | |
int _newWidth = Mathf.RoundToInt(source.width * _scaleFactor); | |
int _newHeight = Mathf.RoundToInt(source.height * _scaleFactor); | |
Color[] _scaledTexPixels = new Color[_newWidth * _newHeight]; | |
for (int _yCord = 0; _yCord < _newHeight; _yCord++) | |
{ | |
float _vCord = _yCord / (_newHeight * 1f); | |
int _scanLineIndex = _yCord * _newWidth; | |
for (int _xCord = 0; _xCord < _newWidth; _xCord++) | |
{ | |
float _uCord = _xCord / (_newWidth * 1f); | |
_scaledTexPixels[_scanLineIndex + _xCord] = source.GetPixelBilinear(_uCord, _vCord); | |
} | |
} | |
// Create Scaled Texture | |
Texture2D result = new Texture2D(_newWidth, _newHeight, source.format, false); | |
result.SetPixels(_scaledTexPixels, 0); | |
result.Apply(); | |
return result; | |
} | |
``` | |
------ OnTriggerStay 에서 프레임단위로 계속 특정 수치를 주려면 반드시 rigidbody 의 sleeping Mode를 Never Sleep으로 바꿔야한다. ------- | |
기본 Start Awake로 두면 들어온 오브젝트가 가만히 있을때 Stay에 잡히지 않는다. | |
------ ?? 연산자 ------ | |
A ?? B | |
A가 null이 아닐 경우는 그대로 A가 null 일경우 초기값을 주고싶을때 B로 줄 수 있다 은근 많이 쓸수 있는 편 | |
A ??= B 역시 A가 null일 경우 B를 집어넣는 코드 | |
------ Network에서 복잡한 Physics 를 공유해야할 때 ------ | |
1. Network Object에 충돌이 발생했을 때 충돌을 일으킨 주체가 Network Player라면 그 Network Player가 서버에 Network Object에 대한 운동값을 공유한다. | |
2. Network Object의 물리값을 스크립트로 변경할 시 그 이후 반드시 변경한 Network Player가 서버에 변경된 운동값을 공유한다. | |
------ Physics 2D 에서 Gravity 0 이라 Physics Material 이 적용 안될 때 ----- | |
Velocity Threshold 1 -> 0.01로 바꾸면 적용 잘 됨. | |
------ UI Mananger를 사용할 경우 씬이 바뀔때 sceneLoaded 에 씬을 체크해서 변수나 함수등을 리서치해서 붙여주는 Action을 붙여 작업을 하면 됨 ------ | |
ex) | |
private void OnEnable() { | |
SceneManager.sceneLoaded += OnSceneLoaded; | |
} | |
private void OnDisable() { | |
SceneManager.sceneLoaded -= OnSceneLoaded; | |
} | |
private void OnSceneLoaded(Scene _scene, LoadSceneMode _mode) { | |
//씬 바뀔때마다 이 함수를 실행 여기서 변수값이나 등등 초기화 하도록! | |
} | |
------ 2D Animation Sprite 만 바꿀 때 ------ | |
Sprite Library, Sprite Resolver 를 활용해서 Sprite를 관리하면 쉽게 Animation의 Sprite를 바꾸는 것을 쉽게 적용할 수 있다. | |
------ Android SDK 새버전 설치할 때 sdkmanager.bat 실행 명령어 이용해서 안드로이드 스튜디오 설치 없이 cmd에서 처리 가능하다 -------- | |
cmd는 관리자 권한으로 실행하고 환경변수 JAVA 유니티 버전에 있는 OpenJDK 폴더 복제해서 C같은곳에 붙여놓고 처리한 후 작업하면 됨 | |
------ 이제 AssetBundle 안쓴다. Addressable 사용함 ----- | |
Resource 에서 Addressable로 갈아타보면 좋을듯 | |
------ 케릭터 밑 UI, 곡선에 따라 UI는 Decal이라는 걸로 표현한다 (지형에 따라 달라지는, 메쉬위에 그리려한다면) ------- | |
하 드디어 해결 | |
Easy Decal 플러그인으로 다 할 수 있다. 바닥위에 그리는 Indicator 는 Decal로 표현한다. | |
------ 게임 일시정지 ---- | |
Time.timeScale = 0; 으로 주면 메뉴나 일시정지 상황에서 모든 코루틴/UniRx 멈출수 있다. | |
----- 일정시간 뒤에 함수실행 ------ | |
private void Start() | |
{ | |
// 3.5 초 후에 실행 | |
StartCoroutine(DelayMethod(3.5f, () => | |
{ | |
Debug.Log("Delay Call"); | |
})); | |
} | |
/// <summary> | |
/// 전달 된 처리를 지정 시간 이후에 실행 한다. | |
/// </summary> | |
/// <param name="waitTime">지연시간[밀리초]</param> | |
/// <param name="action">수행할 작업</param> | |
/// <returns></returns> | |
private IEnumerator DelayMethod(float waitTime, Action action) | |
{ | |
yield return new WaitForSeconds(waitTime); | |
action(); | |
} | |
//UniRx 사용하는 경우 | |
// 단지 호출만 하는 경우 | |
// 100 밀리 초 후에 Log를 출력한다. | |
Observable.Timer(TimeSpan.FromMilliseconds(100)) | |
.Subscribe(_ => Debug.Log("Delay call")); | |
// 매개 변수를 전달하는 경우 | |
// 현재 플레이어의 좌표를 500 밀리 초 후에 표시 | |
var playerPosition = transform.position; | |
Observable.Timer(TimeSpan.FromMilliseconds(500)) | |
.Subscribe(_ => Debug.Log("Player Position : " + playerPosition)); | |
실제 시간 기반 - Timer | |
유니티 TimeScale 기반 - TimerFrame 사용하기! | |
단지 처리를 지연 시키고 싶다면 코루틴을 쓰는 것이 편합니다. | |
하지만 처리를 지연시킨 후, 아직 뭔가 처리를 계속 해야 되는 경우라면 UniRx를 사용하여 스트림화 하는 것이 여러모로 쉽다고 생각합니다. | |
그리고 UniRx는 Time.timeScale 의 영향을 받으므로 일시정지 할때도 코루틴처럼 사용할 수 있다. | |
------ 관찰자 패턴 UniRx 로 구현하는거 인지 [시간처리 핵심] -------- | |
더블클릭 체크하는 코드 두 줄로 구현 가능 | |
using UniRx; | |
using UniRx.Trigger; | |
var clickStream = this.UpdateAsObservable().Where(_ => Input.GetMouseDown(0)); | |
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(200))).Where(x=> x.Count > =2).SubscribeToText(_text, x => string.Format("DoubleClick detected!\n Count:{0}", x.Count)); | |
UniRx를 사용하면 [시간]의 취급이 굉장히 간단해 진다!!! 비동기처리 핵심 | |
.Throttle(시간) 특정 시간까지 스트림이 안오면 그 마지막 스트림을 보낸다. | |
.AsObservable() 이벤트를 스트림으로 변경하는 함수 | |
.Subscribe() 스트림에 구독 뭘할지 작성하는 함수 (OnNext, OnError, OnCompleted) (OnNext, OnCompleted) | |
ex) .Subscribe(result => { //OnNext }, ex => { //OnError }, ()=> { //OnCompleted } | |
uGUI용의 Observable와 Subscribe가 준비되어 있다. | |
button.OnClickAsObservable().SubscribeToText(text, _ => "clicked"); 로 클릭시 text 수정 처리가능 | |
.Buffer(3) 3회 동작시마다 처리 .Skip(2) 2회까지 스킵 3회부터 동작 | |
var clickStream = Observable.EveryUpdate() | |
.Where(_ => Input.GetMouseButtonDown(0)); | |
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250))) | |
.Where(xs => xs.Count >= 2) | |
.Subscribe(xs => Debug.Log("DoubleClick Detected! Count:" + xs.Count)); // 이것도 더블클릭 코드 | |
버튼 1,2가 둘다 눌리면 Text 처리 | |
button1.OnClickAsObservable().Zip(button2.OnClickAsObservable(),(b1,b2) => "Clicked!").First().Repeat().SubscribeToText(text, x => text.text + x + "\n"); | |
드래그로 오브젝트 회전시키는 코드도 가능 (몇줄로 가능) | |
지면 도착시 파티클 생성 하는 코드 | |
public class OnGroundedScript : ObservableMonoBehaviour | |
{ | |
public override void Start() { | |
var characterController = GetComponent<CharacterController>(); | |
var particleSystem = GetComponentInChilderen<ParticleSystem>(); | |
UpdateAsObservable() | |
.Select(_ => charactorController.isGrounded) | |
.DistinctUntilChanged() | |
.Where(x=> x) //True만 통과 | |
.Subscribe(_ => particleSystem.Play()); | |
} | |
} | |
매 프레임 값의 변화를 감시한다면 [ObserveEveryValueChanged] 쪽이 심플하다. | |
characterController.ObserveEveryValueChanged(x => x.isGrounded).Where(x => x).Subscribe(_ => Debug.Log("OnGrounded!")); | |
곡면을 이동하게 되면 isGrounded가 격렬하게 변경되는데 이때 Throttle로 무시하면 됨 | |
UpdateAsObservable() | |
.Select(_ => charactorController.isGrounded) | |
.DistinctUntilChanged() | |
.ThrottleFrame(5) | |
.Subscribe(x => throttledIsGrounded = x); | |
WWW -> ObservalbeWWW 코루틴을 사용하지 않아도 됨 | |
ObservableWWW.Get("~~~").Subscribe(result => Debug.Log(result)); | |
www로 텍스쳐 읽어오기 | |
_button.OnClickAsObservable().First().SelectMany(ObservableWWW.GetWWW(resourceURL)).Select(www => Sprite.Create(www.texture, new Rect(0,0,400,400), Vector2.zero)).Subscribe(sprite => { _image.sprite = sprite; _button.interactable=false; }, Debug.LogError); | |
First() 통신 1회만 하게, 클릭스트림을 ObservableWWW의 스트림으로 덮어씌우는 SelectManay(), TimeOut도 추가할수있음 | |
동시에 통신해서 모든데이터가 모이면 처리 | |
var parallel = Observable.WhenAll( | |
ObservableWWW.Get("http://google.com/"), | |
ObservableWWW.Get("http://naver.com/"), | |
ObservableWWW.Get("http://youtube.com/")); | |
parallel.Subscribe(xs => { | |
Debug.Log(xs[0].Substring(0, 100)); //google | |
Debug.Log(xs[1].Substring(0, 100)); | |
Debug.Log(xs[2].Substring(0, 100)); | |
}); | |
FromCoroutine<T>를 사용하면 자유롭게 스트림을 만들 수 있다. | |
Observable.FromCoroutine<int>(observer => TimerCoroutine(observer, 10)).Subscribe(_ => Debug.Log(_)); | |
private IEnumerator TimerCoroutine(IObserver<int> observer, int timeCount) { | |
do | |
{ | |
observer.OnNext(timeCount); | |
yield return new WaitForSeconds(1.0f); | |
}while(--timeCount > 0); | |
observer.OnNext(timeCount); | |
observer.OnCompleted(); | |
} | |
카운트다운 타이머 | |
------ DontDestroyOnLoad 오브젝트는 무조건 루트단에 있어야만 적용됨 ------- | |
DontDestoryOnLoad 자식에 오브젝트 넣으면 씬 바뀔때 레퍼런스 연결 제거 됨 | |
DontDestoryOnLoad 적용한 A 아래 B라는 오브젝트가 있고 스크립트에서 B오브젝트 변수에 넣어놨는데 씬 바꾸니까 변수에 있는 오브젝트 None(Null)로 변경됨. | |
------ 랜덤으로 리스트 셔플하는 방법 ------- | |
var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList(); | |
------ 체력, 수치 등 변경시 UI 바로 업데이트 하게 하는 스크립트 구조 매우 유용한 팁 ------- | |
public Action<float> update; | |
public long NowHp { | |
get => _nowHp; | |
set { | |
_nowHp = value; | |
if(update!=null) update(value); | |
} | |
} | |
target.update = UpdateText; | |
public void UpdateText(float v) { | |
text.text = v; | |
} | |
아니면 그냥 set에 UI변경코드 넣어도됨 Action 쓰지말고 | |
------ 랜덤 확률 가중치 사용하는 코드 ------ | |
private int[] _craftRareWeight = { 10, 50, 200, 600, 1200, 3600, 7340 }; // S A B C D E F weight | |
foreach(var w in calcRareWeight) { | |
_craftTotalWeight += w; | |
} | |
int rFactor = UnityEngine.Random.Range(0, _craftTotalWeight); | |
for(int i=0; i<_craftRareWeight.Length; i++) { | |
if(rFactor<calcRareWeight.ToList().GetRange(0,i+1).Sum()) { | |
return (ItemRank.S - i); | |
} | |
} | |
------ DoTween Animation 코드로 delay 수정해서 재생하면 delay 수정된 내용 적용 안되는 부분 ------------ | |
왜 그런거지? | |
------ Collaborate 에서 .collabignore 파일에서 ignore 지정할 수 있다. ------ | |
아래 하위 내용 모두 ignore 하려면 Ignore/** 이런식으로 처리할 수 있다. | |
------ 오브젝트 풀링 ------- | |
using System.Collections.Generic; | |
/// <summary> | |
/// 오브젝트 풀링 사용 예시 | |
/// private ObjectPool<GameObject> _damageTextPool; | |
/// | |
/// _damageTextPool = new ObjectPool<GameObject>(CreateFloatText,ResetFloatText); | |
/// | |
/// public GameObject CreateFloatText() { | |
/// var floatingText = Instantiate(Resources.Load("Prefs/DamageUI")) as GameObject; | |
/// floatingText.transform.GetChild(0).GetComponent<DG.Tweening.DOTweenAnimation>().onComplete.AddListener(() => { _damageTextPool.PutObject(floatingText); }); | |
/// return floatingText; | |
/// } | |
/// | |
/// public void ResetFloatText(GameObject fText) { | |
/// fText.transform.SetParent(_poolGO.transform); | |
/// } | |
/// | |
/// | |
/// public ObjectPool<GameObject>[] mobPool = new ObjectPool<GameObject>[60]; | |
/// | |
/// Resources.LoadAll<MobData>("Types/Mobs").ToList().ForEach( x => { mobPool[x.Id] = new ObjectPool<GameObject>( | |
/// ()=>{ | |
/// var mobGO = Instantiate(Resources.Load<GameObject>("Prefs/Mobs/Mob"+x.Id.ToString("D2"))); | |
/// mobGO.GetComponent<MobObject>().OnDone = () => { mobPool[x.Id].PutObject(mobGO); }; | |
/// return mobGO; | |
/// }, | |
/// ( y )=>{ y.transform.SetParent(_poolGO.transform); }, | |
/// 10, 10 | |
/// ); | |
/// }); | |
/// </summary> | |
namespace Curookie.Util { | |
public class ObjectPool<T> { | |
const int MAX_CAPACITY = 30; | |
public delegate T Func (); | |
public delegate void Action (T t); | |
Stack<T> buffer; | |
Func createFunc; | |
Action resetFunc; | |
int index; | |
public ObjectPool (Func createFunc, Action resetFunc, int size = 0, int capacity = MAX_CAPACITY) { | |
if (createFunc == null) { | |
return; | |
} | |
this.buffer = new Stack<T> (); | |
this.createFunc = createFunc; | |
this.resetFunc = resetFunc; | |
this.Capacity = capacity; | |
for (int i = 0; i < size; i++) { | |
PutObject (createFunc ()); | |
} | |
} | |
public int Capacity { get; private set; } | |
public int Count { get { return buffer.Count; } } | |
public T GetObject () { | |
if (Count <= 0) | |
return createFunc (); | |
else | |
return buffer.Pop (); | |
} | |
public void PutObject (T obj) { | |
if (Count >= Capacity) | |
return; | |
if (resetFunc != null) | |
resetFunc (obj); | |
buffer.Push (obj); | |
} | |
} | |
} | |
------- Mesh 하나로 합치는 Asset 있다. -------- | |
------- 울퉁불퉁한 메시에 따라 UI를 생성하는 방법 --------- | |
Assetstore 에 Any.UI 를 설치해서 사용한다. | |
Project UI on any uv mapped surface | |
------- 투명한 바닥에 그림자 그리는 법 ------- | |
https://github.com/keijiro/ShadowDrawer 쉐이더를 이용한다. Matte Shadow 라고 부름 | |
-------- Firebase + GooglePlayGameService 연결할 때 ------ | |
파이어베이스 구글플레이게임서비스 GPGS 연동할 때 주의할 점 | |
Firebase 먼저 세팅하는게, 플러그인 들끼리 꼬이지 않는다. Gpgs를 먼저 하지말고, Firebase를 먼저 import | |
Build Settings 에서 | |
Scripting Baakend - IL2CPP | |
Api Compatibility Level - .NET 4.x | |
Target Architectures - ARM64 | |
-------- 2D 각도에 따른 0-360(-180 180)도 값 구하기 ---------- | |
Mathf.Rad2Deg * Mathf.Atan2(y,x) | |
Mathf.Rad2Deg * Mathf.Atan2(x2-x1, y2-y1) 0~180 (12시방향 시작 시계방향으로 ) -180~0 | |
0~360에서 -180 180으로 바꾸는 코드 | |
public float ToAngle(float num) { | |
return (num>180)? num-360: num; | |
} | |
-------- 코드로 Shader 변경 -------- | |
public IEnumerator Invincible() { | |
var meshs = transform.GetComponentsInChildren<MeshRenderer>(); | |
_invincibleState = InvincibleState.On; | |
gameObject.layer = 6; | |
foreach(var _mesh in meshs) { | |
foreach(var _mat in _mesh.materials) { | |
_mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); | |
_mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); | |
_mat.SetInt("_ZWrite", 0); | |
_mat.DisableKeyword("_ALPHATEST_ON"); | |
_mat.DisableKeyword("_ALPHABLEND_ON"); | |
_mat.EnableKeyword("_ALPHAPREMULTIPLY_ON"); | |
_mat.renderQueue = 3000; | |
_mat.DOColor(new Color(_mat.color.r, _mat.color.g, _mat.color.b, 0.3f), 0.2f).SetLoops(5, LoopType.Yoyo); | |
} | |
} | |
yield return new WaitForSeconds(1.0f); | |
_invincibleState = InvincibleState.Off; | |
gameObject.layer = 3; | |
foreach(var _mesh in meshs) { | |
foreach(var _mat in _mesh.materials) { | |
_mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); | |
_mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero); | |
_mat.SetInt("_ZWrite", 1); | |
_mat.DisableKeyword("_ALPHATEST_ON"); | |
_mat.DisableKeyword("_ALPHABLEND_ON"); | |
_mat.DisableKeyword("_ALPHAPREMULTIPLY_ON"); | |
_mat.renderQueue = -1; | |
_mat.color = new Color(_mat.color.r, _mat.color.g, _mat.color.b, 1.0f); | |
} | |
} | |
} | |
-------- Manager 싱글톤 사용 프리셋 --------- | |
public class GameManager : MonoBehaviour { | |
// 유일한 인스턴스 변수 | |
private static GameManager inst; | |
// 앱 켜졌는지 여부용 | |
private static bool alive = true; | |
/// <summary> | |
/// 속성 싱글톤 패턴으로 구현 | |
/// </summary> | |
public static GameManager Inst { | |
get { | |
// 앱이 꺼젔거나 Destroy됬는지 체크 | |
if (!alive) { | |
Debug.LogWarning (typeof (GameManager) + "' is already destroyed on application quit."); | |
return null; | |
} | |
//C# 2.0 Null 병합연산자 | |
return inst ?? FindObjectOfType<GameManager> (); | |
} | |
} | |
void Awake () { | |
if (inst == null) { | |
inst = this; | |
} else if (inst != this) { | |
Destroy (this.gameObject); | |
} | |
} | |
void OnApplicationQuit () { | |
alive = false; | |
} | |
} | |
-------- ScriptableObject 값 한번에 Code로 변경하고 싶을 때 -------- | |
using UnityEditor; | |
EditorUtility.SetDirty(obj); 하고 변경하면 저장됨. | |
ex) | |
var Datas = Resources.LoadAll<FurnitureData>("Types/Items"); | |
for(int i=101; i<144; i++) { | |
var obj = Datas.ToList().Find(x=> x.Id == i); | |
EditorUtility.SetDirty(obj); | |
obj.Price = FurniturePriceCalc(FetchDrawingDataById(i-100),1); | |
} | |
-------- Edit Mode에서 Play Mode 값 체크하고싶을 때 --------- | |
클래스 앞에 [ExecuteInEditMode] 붙이면 Awake 나 Update에 원하는 함수를 넣으면 실행하지 않고(Play Mode 상태를) 적용시킬 수 있다. | |
-------- 인스팩터 뷰에서 List나 Array에 여러개 요소 한번에 드래그해서 넣고싶을 때 ----- | |
인스펙터 뷰 오른쪽 위 잠금누른 상태에서 넣을 요소 선택해서 드래그해서 넣으면 한번에 넣을 수 있음. | |
------- DOTween Animaiton 문제 ------- | |
DO~~ 함수가 안먹으면 ID를 사용해서 DO~~ByID 함수를 사용해서 진행하면 됨. | |
------ umotion 이라는 에셋 ----- | |
3D 모델링 된거 bone 기반으로 유니티에서 애니메이션 만들 수 있게 해주는 툴 | |
----- 아이템 정보 클래스 ------ | |
ItemInfo 클래스 설계 시 | |
(scriptable object) itemData 아이템 기본정보 | |
(string) itemAmount 아이템 수량인데 여기서 string으로 확률적인 것도 포함시키는 식으로 설계할 수도 있음. | |
ex) "0:3" 앞이 0이면 그냥 3개 "1:3:5" 앞이 1이면 3-5개 랜덤, "2:1:4:0.1" 앞이 2면 기본 1개 최대 4개까지 각각 10% 확률로 | |
이런식으로 포맷을 클래스 안에 넣어주면 ItemInfo만 가지고 아이템 획득, 제거 등에서 쉽게 처리할 수 있다. | |
단점은 단순 계산이 많을 경우 별로임. 그냥 딕셔너리 Key-Value Hash 쓰는게 좋을수도 | |
------ Safe Area 갤럭시 S10 같은 카메라 영역 해제하는법 해상도 ---------- | |
빌드세팅에서 Resolution and Presentation > Render outside safe area 해제 | |
----- c# 현재시각 timestamp 구하기 (UTC 기준) ------- | |
using System; | |
var unixTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); | |
----- 사운드 압축 최적화 ----- | |
1. 모바일에서 스테레오 및 고사양은 의미가 없다 | |
wave, aiff : 44khz가 아니더라도 일반적으로 음성 영역에서는 22khz(절반용량)도 잘 동작, | |
특히 폭발음이나 충격음 등등 중저음 계열은 11khz(1/4용량)로도 잘 동작 | |
mp3, ogg : 압축률을 96kbps 이하로 조정하여 낮춤 | |
2. 사운드 파일을 임포트하면 기본적으로 3D 사운드로 설정이 된다 | |
모바일에서는 굳이 3D 사운드가 필요하지 않으므로 2D 사운드로 변경하자 | |
3. 압축 사운드 (mp3, ogg), 비압축 사운드 (wav, aiff) 구별 | |
순간적인 효과음, 이펙트 (비압축 사운드) : wav, aiff | |
배경 음악 (압축 사운드) : mp3, ogg (경우에 따라 시작시 딜레이가 발생 할 수 있기에 게임에 영향을 가장 덜 받을 수 있는 배경음악에 사용) | |
출처: https://mmzzuu.tistory.com/29 [MMZZUU Company] | |
----- 텍스쳐 최적화 ------ | |
1. 배경 크기는 최종 스크린사이즈에 맞게 | |
제작물에 따라 다르겠지만 현재 아이폰 7+ 의 경우 1920 X 1080으로, 그보다 큰 배경은 사실상 의미가 없습니다. | |
몇몇 고해상도가 필요한 경우 외에는 1/2 X 1/2 또는 2/3 X 2/3 사이즈로 사용해도 충분합니다. | |
2. 캐릭터 최종크기를 고려한 사이즈 | |
화면에서 캐릭터가 차지하는 사이즈를 고려하여 굳이 화면해상도 크기만한 텍스쳐를 만들 필요는 없겠죠. | |
캐릭터의 땀구멍까지 보일정도로 Zoom In하는 연출이 많지않다면 주로 화면에서 차지하는 비율에 맞게 텍스쳐사이즈를 줄여도 큰 차이를 보이지 않습니다. | |
3. iOS 빌드에서 지원하는 PVRTC 압축 포멧을 적극 활용 | |
빌드에서 iOS로 플랫폼을 바꾸고 나면, 임포트된 모든 텍스쳐 메뉴에서 PVRTC압축모드를 사용 할 수 있게 됩니다. | |
빌드 메뉴를 변경한ㄴ 후 모든 텍스쳐를 제대로 설정하게 되면 최적화면에서 꽤 이득을 볼 수 있습니다. (20~30% 감소효과) | |
가로세로 크기는 2의 배수(32X32 , 64X64, 128X128 ...) 로 맞춰야합니다. 맞지않을 경우 다시 사이즈조정하는 처리를 하므로 쓸데없는 처리공정이 생깁니다. | |
알파값이 필요없는 텍스쳐는 거기에 맞게 압축모드를 설정하면 용량이 더 효과적으로 줄어듭니다. | |
출처: https://mmzzuu.tistory.com/29 [MMZZUU Company] | |
----- 매개변수 가변으로 여러개 받을 때 params 키워드를 붙여주면 된다. ----- | |
------ c#은 Linq 함수와 람다를 이용하면 개편해진다. ------- | |
List/Array의 Find,FindAll,ForEach,GetRange 등등.. | |
------ Action에 매개변수 함수 쓰고 싶을 때 ------ | |
public void CallFunc(Action A) { | |
A(); | |
} | |
이런 코드가 있는데 매개변수가 있는 함수를 받는 Action 만드려면 | |
만약 정해진 매개변수이면 그냥 | |
public void CallFunc(Action<int> A, int parm) { | |
A(parm); | |
} | |
제너릭 쓰고 싶다면 | |
public void CallFunc<T>(Action<T> A, T parm) { | |
A(parm); | |
} | |
------ UI 캠, World 캠 분리되어있을 때 World 오브젝트 UI와 연동시켜야할 때 ------ | |
귀찮지만 world 오브젝트를 투영하고있는 캠으로 WorldToScreen하고(z=100하는거 포함) 그 Pos 값을 | |
UI캠으로 ScreenToWorld 해주면 된다. | |
------ UV (UVW의 약자로 각각 XYZ를 의미한다. 텍스처를 생성할 때 모델링의 각 꼭짓점에 대한 좌표를 2D 평면상의 전개도에 나타낸 것을 의미) ------- | |
UV에 대해 알아놓을 필요가 있다. 모델링 된 오브젝트의 색이나 텍스쳐를 변경하는 작업이나 이럴 때 필요. | |
------ 프로젝트 탭에 프리팹 만들 때/프로젝트 탭 하이라키 탭 옮겨다니는 작업할 때 팁 ------ | |
프로젝트 탭을 자주 쓰거나 하이라키 탭과 옮겨다녀야 할 경우 | |
프로젝트 탭을 하나 더 키거나 자물쇠를 이용해서 작업하는게 좋다. | |
------ 클리커 수치 공식 ------- | |
클리커 1.07 and 1.15 제곱으로 구하는 경우가 보통 | |
------- Hierarchy에서 Project로 매번 옮기지 않고 선택한 오브젝트 모두 한번에 개별 프리팹으로 만들고 싶을 때-------- | |
Editor 코드 짜서 가능. | |
// Creates a prefab at the given path. | |
// If a prefab already exists it asks if you want to replace it | |
using UnityEngine; | |
using UnityEditor; | |
public class CreateNewPrefab : EditorWindow | |
{ | |
[MenuItem("Prefab/Create New Prefab")] | |
static void CreatePrefab() | |
{ | |
GameObject[] objs = Selection.gameObjects; | |
foreach (GameObject go in objs) | |
{ | |
string localPath = "Assets/" + go.name + ".prefab"; | |
if (AssetDatabase.LoadAssetAtPath(localPath, typeof(GameObject))) | |
{ | |
if (EditorUtility.DisplayDialog("Are you sure?", | |
"The prefab already exists. Do you want to overwrite it?", | |
"Yes", | |
"No")) | |
{ | |
CreateNew(go, localPath); | |
} | |
} | |
else | |
{ | |
Debug.Log(go.name + " Prefab Created"); | |
CreateNew(go, localPath); | |
} | |
} | |
} | |
// Disable the menu item if no selection is in place | |
[MenuItem("Prefab/Create New Prefab", true)] | |
static bool ValidateCreatePrefab() | |
{ | |
return Selection.activeGameObject != null; | |
} | |
static void CreateNew(GameObject obj, string localPath) | |
{ | |
Object prefab = PrefabUtility.CreateEmptyPrefab(localPath); | |
PrefabUtility.ReplacePrefab(obj, prefab, ReplacePrefabOptions.ConnectToPrefab); | |
} | |
} | |
------- Error 없는데 VS Code Intellisence 먹통 될때 ------- | |
Unity3D와 VSCode를 사용하다보면 어쩌다가 갑자기 IntelliSence 가 먹통이 될 경우가 있다. | |
이유는 C# Extension의 버전업이 되어 그게 작동하지 않는다는 것이다. | |
이를 해결하기 위해선 낮은 버전의 Extension을 재 설치하여야 한다. | |
다음 파일을 받고 아래와 같이 설치를 진행 하도록 하자. | |
csharp-1.15.2.vsix | |
3.18MB | |
1. Download version 1.15.2 (see link above, or the releases page) | |
2. Uninstall the C# extension in VSCode | |
3. Ctrl+Shift+P Extensions: Disable Auto Updating Extensions Enter | |
4. Ctrl+Shift+P Extensions: Install from VSIX...Enter | |
4. Reload VSCode | |
또는 | |
다음과 같이 선택하여 작동 되는 버전으로 재 인스톨을 한다. | |
버전 vsix 파일 받는 법 | |
https://${publisher}.gallery.vsassets.io/_apis/public/gallery/publisher/${publisher}/extension/${extension name}/${version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage | |
https://ms-dotnettools.gallery.vsassets.io/_apis/public/gallery/publisher/ms-dotnettools/extension/csharp/1.21.12/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage | |
응용하면 된다. ms-dotnettools 의 csharp 플러그인의 1.21.12버전을 받는 URL이다. | |
.vsixpackage 확장자를 .vsix로 바꿔주면 된다. | |
또 하나의 팁은 "Assets > Reimport All"을 이용해서 다시 불러오는 거다. | |
------- GitLab 무료 프라이빗 용량은 10GB GitHub는 1GB --------- | |
.gitignore 할 때 에셋 폴더 다 제외해주자 용량과 쓸대없는 에셋도 올라가기 때문에 용량도 커지고 여러모로 | |
나중에 프로젝트 받아서 에셋 설치하면 됨. | |
------- XML이나 ScriptObject 사용해서 텍스트 처리할 때 줄바꿈 문제 --------- | |
.Replace("\\n", "\n"); 붙이면 해결 | |
------ Confetti 흩뿌리는 효과, Radial Light 빛 돌아가는 효과 ( or item sparkle) ------- | |
용어 알아 두자 | |
------ Light 빛 ----- | |
간접 조명 Indirect | |
직접 조명 Direct | |
- Realtime => 씬에 직접 광을 정용하며 매 프레임 조명 연산을 해서 화면을 업데이트한다. 연산 부하가 크고, 간접 광원을 사용하지 않아 그림자는 검은색으로 표현한다. | |
- Mixed => 직접 광원과 간접 광원 모두를 사용한다. | |
- Baked => 런타임 시 조명 연산 처리를 하지 않는 조명이다. 정적(Static)인 물체에만 조명 연산을 처리해 표면의 조명과 그림자를 생성한다. 동적인 물체는 라이트 프로브를 이용해 적용한다. | |
정적인 오브젝트는 static object 처리해주는게 좋다. | |
Baking 하면 나오는 파일을 GI Cashe라 하며 Unity Preferences 창에서 저장 경로와 사이즈 설정이 가능하다. | |
Intensity Multiplier 속성은 반사율을 나타낸다. | |
실시간 전역 조명을 쓰지 않으면 Realtime Lighting 언체크 | |
Subtractive로 설정하면 라이트맵과 라이트 프로브, 그림자 모두 하나의 라이트맵에 베이크 된다. | |
- Mixed Lighting => Baked Global Illumination 체크 | |
- Lighting Mode => Subtractive 설정 | |
Auto Generate | |
실시간 라이트매핑 옵션을 비활성화하자. | |
모바일에서는 성능 저하를 일으키는 원인이다. | |
Lightmap Resolution은 유닛당 텍셀값으로, | |
이 값을 증가시키면 라이트맵의 품질은 높아지나 베이크 시간이 증가하고, | |
라이트맵의 개수가 많아진다. | |
Lightmap Size 속성은 라이트맵의 크기를 결정하며, 하드웨어 성능을 고려해 | |
적절한 값을 찾아야 한다. | |
라이트맵 베이크가 완료되면 라이팅 뷰 상단의 Global Maps안에 베이크된 텍스처 확인이 가능하며, | |
프로젝트 뷰에 해당 라이트맵 파일들이 생성된 걸 알 수 있다. | |
Auto Generate 옵션을 끄고 진행할 때는 수정사항이 생겼을 대 매번 베이크하는게 번거롭기 때문에 | |
최종 작업물에만 수동으로 베이크하는 걸 권유한다. | |
------ 자기만의 프레임워크를 만들어서 static 클래스로 사용하면 편하다. ------- | |
ex) string 처리, 타이머 등등 | |
------ 게임 내에서 실제 시간 접목할 때 ------- | |
게임에서 가장 많이 쓰이는 하트 .. 하트는 10분이 지나면 채워지는데, 50분 지나서 로그인 하면 5개 채워져있어야 합니다. | |
이를 위해서는 DateTime을 활용합니다. | |
DateTime 은 float형이 아닙니다. | |
사용을 위해서는 using System; 이 필요합니다. | |
만약, 하트가 줄어들시점에서 | |
DateTime OldTRime = System.DateTime.Now; | |
를 지정하셨다면, 하트가 줄어들 시점의 타임이 저장됩니다. | |
DateTime LoginTime = System.DateTime.Now; | |
위와 같이 LoginTime 이 있다고 칩니다. | |
LoginTime - OldTRime 을 DateTime으로 받을 수 없습니다. | |
몇분일지 몇초일지 모르는 지나간 시간이 나오기 때문인데; 이를 받는것이 TimeSpan 입니다. | |
TimeSpan AAA = LoginTime - OldTRime; | |
이렇게 받으시면 흐른 시간을 구할 수 있습니다 | |
사용방법 | |
if (AAA.TotalSeconds > 24 * 60 * 60) | |
TotalSeconds 로 받으면 초 단위를 받게 되는데, 24 * 60 * 60 초는 하루 입니다. 위의 if문은 하루보다 크면, 하루가 지나면? 이 되겠습니다. | |
날자 단위로 하고 싶다면 Day를 쓰시면 됩니다. | |
위의 값은 Double 형 입니다. | |
유니티의 Time.realtimeSinceStartup은 이런저런 용도로 쓸 곳이 많다. | |
timeScale의 영향을 받지 않기도 하고, 게임이 백그라운드로 내려간 상황에서도 시간을 카운트하므로 | |
실시간 서버를 사용할 수 없는 환경에서 시간을 시뮬레이션하는 용도로도 이용 가능하다. | |
하지만 2017.4.26f1 버전 기준으로 안드로이드에서는 치명적인 버그가 있다. | |
앱을 백그라운드로 내리는 부분은 괜찮은데, | |
앱을 띄운 상태에서 화면을 껐다가 다시 켜면 realtimeSinceStartup이 제대로 카운트되지 않는 문제다. | |
Monotonic Time | |
이런저런 조사를 해 본 결과 클라이언트에서 유저가 설정한 시간과는 별개로 실제 시간을 시뮬레이션하기 | |
위해서는 일반적으로 다음과 같은 방법이 많이 쓰이는 것으로 보인다. (*모든 로직 실행에 서버를 통하는 케이스는 논외) | |
서버에서 시간을 받아온 뒤 클라이언트에 저장한다.(Timezone 문제를 피하기 위해 보통 UTC Time을 이용) | |
서버에서 시간을 받아온 순간의 하드웨어 Uptime을 같이 저장한다. | |
하드웨어 Uptime을 가지고 Elapsed Time을 측정해 시간을 시뮬레이션한다. | |
하드웨어 Uptime은 하드웨어가 부팅된 이후 지난 시간을 의미한다. | |
위와 같은 방법을 쓰면 클라이언트에서 유저의 시간 조작 여부와 관계없이 실제 시간을 시뮬레이션할 수 있다. | |
하드웨어 Uptime은 개념상 유저가 시간을 변경하든, 화면이 꺼져 있든 상관없이 언제나 실제 시간에 따라 누적되기 때문이다. | |
다만 위에서 언급했듯이 현실에서는 이러한 Uptime 카운터 역시도 하드웨어의 클럭이나 이런저런 조건에 영향을 받으므로, | |
오차가 점점 누적되기 시작하면 적게는 1~2초에서 많게는 몇 분정도 실제 시간과 차이가 날 수 있다. | |
따라서 일정 시간 간격을 두고 꾸준히 서버와 Time Sync를 맞추는 로직이 필요하다. | |
또한 팁을 한 가지 더 쓰자면, 네트워크에 타임 싱크용 패킷이 왔다갔다 하는 시간도 고려해야 함을 기억하자. | |
예를 들어 클라이언트가 11시 59분 59초에 타임 싱크 요청을 보냈고 서버에 12시 정각에 도착했다면 응답 값은 12시겠지만, | |
Timeout을 얼마로 설정했는지에 따라 클라이언트가 이 응답 값을 받는 시간은 12시 00분 1초일수도, 12시 00분 30초일 수도 있다는 것이다. | |
따라서 타임 싱크용 요청은 상대적으로 짧은 Timeout 값을 두어야 한다. | |
하드웨어의 Uptime과 같이 지속적으로 일정하게 증가하는 시간을 Monotonic Time이라고 부른다. | |
------- ScriptableObject 에서 Dictionary나 Tuple같은거 Inspector 에 표현하고 싶을 때 ---------- | |
직렬화 클래스 하나 만들어서 리스트나 배열로 선언하면 Inspector에 표기됨. | |
이걸 응용해서 EditorWindows에서도 이용할 수 있다. SerializeProperty도 이런식으로 구현가능. | |
ex) | |
public List<A> a; | |
[System.Serializable] | |
public class A { | |
public string elem1; | |
public int elem2; | |
} | |
------- 게임 시간 초단위 ??:?? 표현 코드 ------- | |
private IEnumerator StartTimer() { | |
int minutes; | |
int seconds; | |
int fraction; | |
Debug.Log("start"); | |
while(true) { | |
minutes = Mathf.FloorToInt(timer / 60F); | |
seconds = Mathf.FloorToInt(timer - minutes * 60); | |
fraction = Mathf.FloorToInt(timer * 100f) % 100; | |
timeTxt.text = string.Format("{0:00}:{1:00}.{2:00}", minutes, seconds, fraction); | |
yield return null; | |
timer+=Time.deltaTime; | |
} | |
} | |
-------- Random Rare도 수치 정할 때 Weigth 배열 두고 ------ | |
수치를 반복문으로 다 더해서 토탈만큼 Random 돌리고 낮은 순서대로 조건문으로 판별하는 식으로 구현하는게 기본적임 | |
-------- 반응형 텍스트 상자 uGUI 만들고 싶으면 ------- | |
Panel -> Horizontal Layout Group or Vertical Layout Group (Control Child Size W/H, Child Force Expand W/H 체크), Content Size Fitter( V Preferred Size ) | |
└ Text -> H Wrap, V Overflow 해주면 됨. | |
-------- gui에서 %로 비율을 정하고싶으면 shift+alt Rect풀차징해놓고 Anchers Min Max 0-1사이값 조정하면 된다.---------- | |
반응형 디자인 가능 | |
-------- UI Screen Space - Camera 쓸 때 UI의 position 값이 이상한데 ScreenToWorld 써도 안됨 이때 Distance값만큼 벡터에 추가해서 계산하면 됨 ----- | |
Screen Space - Overlay 모드를 사용하면서 작업 했는데, | |
이제 Screen Space - Camera 모드로 변경하면서 작업하니깐 문제가 하나 생겼습니다. | |
Prefab 을 Instantiate 으로 생성할경우 UI Position 값이 위치, 스케일 값이 이상하게 변경되었습니다. | |
그래서 Instantiate 를 하고 localPosition 을 초기화 해주는 방식으로 맞췄습니다. | |
근데 이런 UI 를 드래그 앤 드랍을 하기위해 position 을 input.mousePosition 으로 할때 생기는 문제를 해결하지 못하겠습니다. | |
UI의 position 값들이 기존 Screen Space - Overlay 에서 사용했던 값들이랑 많이 달라져서 문제가 발생합니다 ㅠㅠ | |
따라서 월드 포지션의 값을 UI position 으로 변환하는 방법도 찾아 보고 했는데 해결하지 못했습니다. | |
캔버스 오브젝트에서 Screen Space - Camera모드를 사용하면 Plane Distance 값을 지정하는데 이때 | |
var screenPoint = new Vector3(Input.mousePosition.x,Input.mousePosition.y,100.0f); // z값을 Plane Distance 값을 줘야 합니다!! | |
transform.position = uiCamera.ScreenToWorldPoint(screenPoint); // 그리고 좌표 변환을 하면 끝! | |
반대로 UI Camera 쓰는데 UI 위치를 Screen 데이터로 찾고 싶으면, GetWorldCorners로 가능 | |
Vector3[] corners = new Vector3[4]; | |
colorPickerRT.GetWorldCorners(corners); | |
Vector2 mousePos = new Vector2(Input.mousePosition.x, Input.mousePosition.y); | |
Vector2 localImagePos = (mousePos - (Vector2)corners[0]) / mainUIRT.localScale.x; | |
-------- C# List = List 이런식으로 복사연산자 쓰면, 얕은복사가 일어나 값이 수정된다. -------- | |
List<int> aList = new List<int>(); | |
List<int> bList = aList; | |
bList.Add(100); | |
Debug.Log(aList[0]); //bList의 변화가 aList에도 똑같이 영향을 줘서 100이 찍힘. | |
원본 리스트에는 영향을 주지 않고, 값만 복사하고 싶을 수 있는데요, | |
그럴땐 System.Linq 에 있는 List.ToList() 함수를 사용해줍니다. | |
System.Linq; | |
Class ForList() | |
{ | |
List<int> aList = new List<int>(); | |
List<int> bList = aList.ToList(); | |
bList.Add(100); | |
Debug.Log(aList.Count); //aList에는 변화가 없으므로 0이 찍힘. | |
} | |
깊은복사는 | |
If you do a shallow copy like | |
List<int> x = new List<int>() { 1, 2, 3, 4, 5 }; | |
List<int> y = x; | |
y[2] = 4; | |
Then x will contain {1, 2, 4, 4, 5 } | |
If you do a deep copy of the list: | |
List<int> x = new List<int> { 1, 2, 3, 4, 5 }; | |
List<int> y = new List<int>(x); | |
y[2] = 4; | |
immutable 타입일 경우 | |
var deepList = list.ConvertAll(s => s); | |
클래스 리스트 같은 경우 (mutable 타입일 경우) | |
Clone 함수를 만들어놓거나 | |
public class ItemData : ScriptableObject, ICloneable | |
{ | |
public int Id; | |
public string Name; | |
public string Description; | |
public ItemType Type; | |
public long Price; | |
public bool Stackable; | |
public ItemRank Rank; | |
public AbilityType Ability; | |
public float AbilityValue; | |
public Sprite Sprite; | |
public ItemData() | |
{ | |
this.Id = -1; | |
} | |
public object Clone() { | |
ItemData copy; | |
if(this.Type==ItemType.Furniture) { | |
copy = new FurnitureData(); | |
((FurnitureData) copy).Size = ((FurnitureData) this).Size; | |
((FurnitureData) copy).IsOccupied = ((FurnitureData) this).IsOccupied; | |
((FurnitureData) copy).InteriorType = ((FurnitureData) this).InteriorType; | |
} else { | |
copy = new ItemData(); | |
} | |
copy.Id = this.Id; | |
copy.Name = this.Name; | |
copy.Type = this.Type; | |
copy.Price = this.Price; | |
copy.Description = this.Description; | |
copy.Stackable = this.Stackable; | |
copy.Rank = this.Rank; | |
copy.Ability = this.Ability; | |
copy.AbilityValue = this.AbilityValue; | |
copy.Sprite = this.Sprite; | |
return copy; | |
} | |
} | |
var deepList = list.ConvertAll(o => new TestObject(o.TestValue)); | |
List<Book> books_2 = books_1.Select(book => new Book(book.title)).ToList(); | |
List<Book> books_2 = books_1.ConvertAll(book => new Book(book.title)); | |
이런식으로 해야한다. | |
-------- UI 현재 화면의 px 사이즈 가져오는 법 (이미지 실제 사이즈 가져오기)---------- | |
var a = new Vector3[4]; | |
imageA.GetComponent<RectTransform>().GetWorldCorners(a); | |
for(int i=0; i<4; i++) { | |
Debug.Log(Camera.main.WorldToScreenPoint(a[i])); | |
} | |
이러면 이미지의 스크린 위의 4개의 점 px값을 가져올 수 있다. | |
-------- EditorWindow (확장 윈도우) 종합 팁 -------------- | |
using UnityEditor 를 사용한다. | |
[MenuItem("플러그인/SpotTheDifference")] 이런식으로 위 상단에 추가할 수 있다. | |
static void ShowWindow() { | |
var window = GetWindow<SpotTheDifferenceEditor>("틀린그림 찾기"); | |
window.Show(); | |
} | |
OnGUI() 에 꾸미는 내용들은 넣는다. | |
GUILayout.Label() | |
GUILayout.Button() | |
selected = GUILayout.Toolbar (selected, new string[] { "1", "2", "3", }, EditorStyles.toolbarButton); | |
UILayout.Label("설정", EditorStyles.boldLabel); | |
EditorGUILayout.BeginHorizontal(); | |
GUILayout.Label("게임 수"); | |
gameCount = EditorGUILayout.IntSlider(gameCount, 1, 6); | |
EditorGUILayout.EndHorizontal(); | |
EditorGUILayout.BeginHorizontal(); | |
GUILayout.Label("게임 당 시간"); | |
gameTime = EditorGUILayout.Slider(gameTime, 5.0f, 60.0f); | |
EditorGUILayout.EndHorizontal(); | |
EditorStyles.boldLabel 굵은 글씨 | |
1.1 Editor 폴더 | |
Editor폴더는 에디터 API를 사용하기 위한 특별한 폴더. | |
보통 에디터 API는, 런타임으로 동작하지 않음. | |
아래 코드를 Assets 폴더 바로 아래에 작성해서 빌드해보면 빌드 실패 뜸. | |
1.2 Editor Default Resources 폴더 | |
Resources 폴더랑 마찬가지로 에디터 확장에서만 사용할 리소스를 넣어둘 수 있는 폴더임. | |
Editor Default Resources 폴더 안에 있는 에셋은 EditorGUIUtility.Load로 접근 가능. | |
var tex = EditorGUIUtility.Load ("logo.png") as Texture; | |
static int gameCount = 4; //변수는 스태틱 붙이기 디폴트값 때문에 | |
static float gameTime= 10f; | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEditor; | |
using System.IO; | |
using UnityEngine; | |
public class GameDataEditor : EditorWindow | |
{ | |
public GameDataJS gameDataJS; | |
private string gameDataProjectFile = "/StreamingAssets/data.json"; | |
[MenuItem ("Window/Editor Hry Pro Alenku")] | |
static void Init() | |
{ | |
GameDataEditor window = (GameDataEditor)EditorWindow.GetWindow(typeof(GameDataEditor)); | |
window.Show(); | |
} | |
private void OnGUI() | |
{ | |
if (gameDataJS != null) | |
{ | |
SerializedObject serializedObject = new SerializedObject(this); | |
SerializedProperty serializedProperty = serializedObject.FindProperty("gameDataJS"); | |
EditorGUILayout.PropertyField(serializedProperty, true); | |
serializedObject.ApplyModifiedProperties(); | |
if (GUILayout.Button("Save Data")) | |
{ | |
SaveGameData(); | |
} | |
} | |
if (GUILayout.Button("Load Data")) | |
{ | |
LoadGameData(); | |
} | |
} | |
private void LoadGameData() | |
{ | |
string filePath = Application.dataPath + gameDataProjectFile; | |
if (File.Exists(filePath)) | |
{ | |
string dataAsJson = File.ReadAllText(filePath); | |
gameDataJS = JsonUtility.FromJson<GameDataJS>(dataAsJson); | |
} | |
else | |
{ | |
gameDataJS = new GameDataJS(); | |
} | |
} | |
private void SaveGameData() | |
{ | |
string dataAsJson = JsonUtility.ToJson(gameDataJS); | |
string filePath = Application.dataPath + gameDataProjectFile; | |
File.WriteAllText(filePath, dataAsJson); | |
} | |
} | |
--------- Delegate에서 변수를 매개변수로 지정하고 싶으면 ------- | |
int b; //allocate here | |
for(int i = 0; i<15; i++) { | |
b = i; | |
listOfButtons[i].GetComponent<Button>().onClick.AddListener(() => Debug.Log(b) ); | |
} | |
모든 경우에서 Debug.Log(15) 값으로 적용됨. | |
int b; | |
for(int i = 0; i<15; i++) { | |
int b = i; //allocate new "instance" EACH Step of loop | |
listOfButtons[i].GetComponent<Button>().onClick.AddListener(() => Debug.Log(b) ); | |
} | |
이런식으로 int 선언을 for문안에 해줘야 한다. 아니면 맨 마지막 값으로 적용됨. | |
The whole loop is run to completion when you call this method, after that, at some point you press the button, and by then the value of i is upgradeText.Length because that's the exit condition for your loop. | |
When you use delegates or such in this manner, the variable i gets wrapped into the delegate rather than just its value, making it sort of act like a reference type. | |
It should be easily fixable by passing a variable you don't change later into the delegate. | |
var i2 = i; | |
buttonList[i].onClick.AddListener(delegate { BuildingPlacer(buildings[i2]); }); | |
--------- Custom Render Texture 버그 ------------- | |
안드로이드 2019.2.16f1 에서 3D Lowpoly 객체를 카메라로 잡으면 뒤에있는 물체가 앞으로 보이는 현상이 있다. | |
해결 못해서 그냥 함 Depth로 | |
---------- NDK il2cpp 에러 나다가 ------------ | |
NDK 직접 다운받아서 다른경로에 풀어서 그 경로 입력해주고 진행하니까 됨. | |
-------- 레벨 벨런스 --------- | |
1.07 and 1.15 제곱 | |
-------- 유니티 모바일 터치 ----------- | |
Input.Touch쓰는게 가장 괜찮고, 에디터에서 테스트 방법은 1. 터치되는 모니터로 하거나, 2. Unity Remote로 하거나, 3. 스크립트 분해해서 Editer는 마우스 클릭으로 코드를 짜는식으로 해야함. ㅡ | |
Input.multiTouchEnable 로 멀티터치 키고끌수있다. | |
Input.Touch는 PC Editor에서는 인식을 안하니까 | |
IPointer를 사용한 안드로이드, PC 플랫폼 통합방법을 사용 근데 터치 인식 별로임 | |
public class mCameraController : MonoBehaviour, IBeginDragHandler, IDragHandler | |
{ | |
Vector3 FirstPoint; | |
Vector3 SecondPoint; | |
public float xAngle = 0f; | |
public float yAngle = 55f; | |
float xAngleTemp; | |
float yAngleTemp; | |
public void BeginDrag(Vector2 a_FirstPoint) | |
{ | |
FirstPoint = a_FirstPoint; | |
xAngleTemp = xAngle; | |
yAngleTemp = yAngle; | |
} | |
public void OnDrag(Vector2 a_SecondPoint) | |
{ | |
SecondPoint = a_SecondPoint; | |
xAngle = xAngleTemp + (SecondPoint.x - FirstPoint.x) * 180 / Screen.width; | |
yAngle = yAngleTemp - (SecondPoint.y - FirstPoint.y) * 90 * 3f / Screen.height; // Y값 변화가 좀 느려서 3배 곱해줌. | |
// 회전값을 40~85로 제한 | |
if (yAngle < 40f) | |
yAngle = 40f; | |
if (yAngle > 85f) | |
yAngle = 85f; | |
transform.rotation = Quaternion.Euler(yAngle, xAngle, 0.0f); | |
} | |
} | |
그냥 모바일용 Input.Touch | |
1. 터치된 손가락 갯수 인식하기 | |
// 손가락 터치가 1개 일때 | |
if(Input.touchCount == 1){ ... } | |
// 손가락 터치가 2개 이상일때 | |
if(Input.touchCount >= 2){ ... } | |
// 손가락 터치가 오직 1개일때만 | |
if(Input.touchCount < 2){ ... } | |
2. 터치의 단계는 5가지 | |
// 터치가 시작되었을 때 | |
if(Input.GetTouch(0).phase == TouchPhase.Began){ ... } | |
// 터치된 손가락이 움직일 때 | |
if(Input.GetTouch(0).phase == TouchPhase.Moved){ ... } | |
// 터치된 손가락이 그자리에 가만히 있을 때 | |
if(Input.GetTouch(0).phase == TouchPhase.Stationary){ ... } | |
// 터치된 손가락이 스크린에서 떨어질 때 | |
if(Input.GetTouch(0).phase == TouchPhase.Ended){ ... } | |
// 모바일폰을 귀에 갖다 대거나 touch tracking을 수행하지 않아야 할 경우에 | |
if(Input.GetTouch(0).phase == TouchPhase.Canceled){ ... } | |
3. 터치의 응용 | |
// 손가락 두 개가 움직일 때 | |
if(Input.GetTouch(0).phase == TouchPhase.Moved && Input.GetTouch(1).phase == TouchPhase.Moved){ ... } | |
// 손가락 하나는 가만히 있고 다른 하나는 움직일 때 | |
if(Input.GetTouch(0).phase == TouchPhase.Stationary && Input.GetTouch(1).phase == TouchPhase.Moved){ ... } | |
// 손가락 한 개가 스크린을 움직일 때 속도 | |
if(Input.GetTouch(0).phase == TouchPhase.Moved){ | |
float speedX = Input.GetTouch(0).deltaPosition.x / Input.GetTouch(0).deltaTime; // 횡방향 속도 | |
float speedY = Input.GetTouch(0).deltaPosition.y / Input.GetTouch(0).deltaTime; // 종방향 속도 | |
} | |
// 스크린 찍은 위치에서 Raycast 할 때 | |
if(Input.GetTouch(0).phase == TouchPhase.Began){ | |
Ray ray = Camera.main.ScreenPointToRay(Input.GetTouch(0).position); | |
RaycastHit hit; | |
if(Physics.Raycast(ray, out hit, 10.0f)){ | |
Instantiate(something, hit.point, Quaternion.identity); | |
} | |
} | |
-------- Buggy Ghost Object 막기 ------- | |
어플리케이션을 끌때 매니저같은 스크립트들에서는 alive라는 bool변수를 둬서 싱글톤에서 사용해서 잘못된 생성을 막는데 | |
OnApplicationQuit에서 alive bool변수를 끄는식으로 마쳐야한다. | |
-------- 쓸만한 유니티 속성 -------- | |
1. RequireComponent : 컴포넌트 자동 추가. | |
[RequireComponent (typeof (Rigidbody))] | |
public class PlayerScript : MonoBehaviour { | |
void FixedUpdate() { | |
rigidbody.AddForce(Vector3.up); | |
} | |
} | |
2. SerializeField : 인스펙터에 비공개 멤버 노출. | |
using UnityEngine; | |
public class SomePerson : MonoBehaviour { | |
//This field gets serialized because it is public. | |
public string name = "John"; | |
//This field does not get serialized because it is private. | |
private int age = 40; | |
//This field gets serialized even though it is private | |
//because it has the SerializeField attribute applied. | |
[SerializeField] | |
private bool hasHealthPotion = true; | |
void Update () { | |
} | |
} | |
3. Header : 인스펙터에서 제목표시 (마크다운 #과 같음) | |
ex) [Header("Background Music Properties")] | |
4. Tooltip : 인스펙터 조종란에 마우스 갔다대면 설명 | |
ex) [Tooltip("Is the background music mute")] | |
[SerializeField] bool _musicOn = true; | |
5. Range(a, b) : a에서 b만큼 범위 정할 수 있음 | |
ex) [Range(0, 1)] | |
[SerializeField] float _soundFxVolume = 1f; | |
6. Space(a) : a만큼 줄 띄우기 | |
ex) | |
[Tooltip("The exposed volume parameter name of the sound effects mixer group")] | |
[SerializeField] string _volumeOfSFXMixer = string.Empty; | |
[Space(3)] | |
[Tooltip("A list of all audio clips attached to the AudioManager")] | |
[SerializeField] List<AudioClip> _playlist = new List<AudioClip>(); | |
------- 코드영역 그룹핑 #region이란 ------------ | |
#region 모듈설명 | |
#endregion 으로 코드를 깔끔하게 정리할 수 있다. | |
vscode에서 ctrl+shift+{ / } 로 열고 닫을 수 있음 | |
------- Scriptable Object 로 데이터를 구조화하면 에디터편집이 쉬워진다. -------- | |
Type Object 패턴하고 엮어쓰기에도 좋음. | |
기획자와 소통하기 좋음. | |
게임 내 데이터는 무조건 이걸로 제작하자. | |
------- 아이콘 작업이나 스프라이트 작업할 때 psd 파일로!! ------- | |
디자이너와 소통할 때 psd파일 자체를 달라고 하면 됨. 투명도 처리나 아이콘 배경 끄고키는것도 유니티내에서 포샵파일열어서 수정가능하니까 | |
psd 파일로 달라하자 psd 파일 바로 스프라이트로 쓸 수 있음. | |
그래픽 압축설정은 | |
remove matte (PSD) 체크 | |
Generate Mip Maps 체크 | |
Aniso Level 1 | |
Max Size 2048 | |
------- UML 툴은 VS Code에 PlantUML 설치하면 끝------ | |
코드로 uml 작성가능하고 generate도 가능하고 여러모로 갑 | |
볼수도 있고 버전관리에 넣을수도 있어서 좋다. | |
.wsd 확장자고 기본 단축키랑 사용문법 익혀서 UML 이걸로 그리자 | |
------- LWPR 핑크색 텍스쳐 깨질 때 ------ | |
대체적으로 쉐이더 충돌일 경우이다. | |
삭제하고 전부 다시 불러오기 | |
기존 빌트인 파이프라인에서 사용되었던 쉐이더들이 LWRP에서 사용 불가능하게 될수도 있다. | |
Edit > Render Pipeline > LWPR > Update Project to LWPR 이거 누르면 한번에 바뀜. | |
그리고 프로젝트에서 Ctrl+A눌러서 오른쪽버튼 reimport 누르면 해결. | |
2019.12. 커스텀 쉐이더 여전히 핑크색으로 나오는 문제 해결 못하겠다. | |
그래픽 API 문제일 경우 | |
Player Settings > Other Settings > Graphics APIs > Auto Graphic API 빼고 OpenGLES3 추가해서 위로 | |
현상 | |
분PC에서 실행한 에디터에서는 정상적으로 화면이 나온다. | |
하지만 안드로이드에서 실행하면 특정 Shader를 사용한 부분이 모두 분홍색으로 나온다. | |
원인 | |
유니티는 확실히 사용되고 있는 Shader만 배포한다. | |
예를들어 Material에 특정한 Shader를 연결하고 이 Material이 특정 오브젝트에 할당된 경우 | |
지금 해보니 Material만 만들고 할당하지 않으면 배포하지 않는듯 | |
그래서 런타임에 Shader.Find 같은 함수로 Shader를 찾는 경우 배포 버전에 포함되지 않아서 화면이 분홍색으로 나올 수 있다. | |
해결 | |
Edit > Project Settings > Graphics | |
Always Included Shaders에서 자신이 사용하는 Shader를 추가한다. | |
혹은 위에서 말했듯이 직접 Material을 만들어서 할당하면 해결이 됨 | |
-------- 빌드 시 Cloth/ Skin Mesh Renderer 사라지는 문제 --------- | |
Unity 2019.2.10f1 에서 Window/Mac 빌드 시 Cloth/ Skin Mesh Renderer 적용한 렌더링이 안 되는 경우 발생 | |
해결 > Build > Other Settings > Dynamic Betching (false -> true) 로 변경하니 빌드 시 나옴. | |
-------- PropertyDrawer 란 -------- | |
유니티는, Serialize된 데이터를 유니티가 자동판단해서 적절한 GUI를 사용해, 인스펙터에 표시합니다. | |
PropertyDrawer는 그 유니티에 의한 자동판단 처리를 Hook해서 스스로 GUI를 사용하기 위한 기술입니다. 이것을 통해 특정 GUI만을 커스터마이즈 할 수 있습니다. | |
인스펙터에 표시되는 컴포넌트의 GUI를 변경하기 위해서는 CustomEditor가 적절합니다. 하지만, 이것은 컴포넌트 전체의 커스터마이즈입니다. | |
지금은, 컴포넌트의 일부인 hp 변수(프로퍼티)만을 커스터마이즈 하고 싶은것이므로 CustomEditor가 아니라 PropertyDrawer를 사용합니다. | |
-------- Runtime에서 script로 Material 변경 시------- | |
materials 을 하나씩 교체는 불가능하다. material[] 배열형태로 집어넣어야함. | |
따라서 하나의 메터리얼만 수정할 때도 materials 가져와서 수정 후 materials 에 통째로 다시 넣어주는 형태로 작업 해야함 | |
ex) | |
Material[] g1m = g1.GetComponent<MeshRenderer>().materials; | |
g1m[0]= mList[0]; | |
g1.GetComponent<MeshRenderer>().materials = g1m; | |
------- 3D Pivot 포인트 수정하는 방법 ------- | |
3D 모델링 피벗 수정하는 법은 빈 Object 하나 넣고 스크립트로 수정 가능하다. | |
using UnityEngine; | |
using System.Collections; | |
public class Gizmo : MonoBehaviour { | |
public float gizmoSize = .75f; | |
public Color gizmoColor = Color.yellow; | |
void OnDrawGizmos() | |
{ | |
Gizmos.color = gizmoColor; | |
Gizmos.DrawWireSphere(transform.position, gizmoSize); | |
} | |
} | |
------- 빌드 사이즈 체크 ------------ | |
빌드 후 Console > Open Editor Log 누르면 | |
용량 차지하는 비율을 볼 수 있다. | |
이걸로 용량 줄일 때 참고하면 유용 | |
ex) Build Report | |
Uncompressed usage by category (Percentages based on user generated assets only): | |
Textures 529.5 mb 93.5% | |
Meshes 8.1 mb 1.4% | |
Animations 220.8 kb 0.0% | |
Sounds 91.7 kb 0.0% | |
Shaders 4.4 mb 0.8% | |
Other Assets 9.1 mb 1.6% | |
Levels 255.5 kb 0.0% | |
Scripts 1.2 mb 0.2% | |
Included DLLs 13.2 mb 2.3% | |
File headers 280.9 kb 0.0% | |
Total User Assets 566.3 mb 100.0% | |
Complete build size 605.8 mb | |
------ 투명한 트리거 표시 오브젝트 or 유리창 간단하게 표현하는 법 ----- | |
에셋 스토어 사용하지 않고 그냥 박스 오브젝트 만들어서 Cast Shadows 는 Off 해주고 투명한 물체/영역이니까 | |
머터리얼 하나 씌운다음 머터리얼 Randering Mode > Opaque -> Transparent 로 변경하고 Albedo 에서 원하는 색상 투명도 적용하면 끝! | |
Smoothness 조정하면 반사느낌을 설정할 수 있다. | |
그리고 Raycast 쏠 때 트리거를 무시하는 방법은 | |
To have your raycasts ignore all trigger colliders in 5.2 go to: | |
Edit > Project Settings > Physics > Uncheck "Queries Hit Triggers" | |
(If you're using an older version of Unity, the check box is called "Raycasts Hit Triggers") | |
I know this is an old topic, but seeing as its the first result when googling the problem it should still be helpful. | |
에디터 프로젝트 세팅에서 물리 > 언체크 힛 트리거 하면 됨. | |
------ 모델링 불러올 때 FBX 파일이 더 지원 잘 된다. OBJ 파일보다 ---- | |
모델러한태 FBX로 추출해달라고 하자. | |
------- 광고주 ---------- | |
iron source 라는 곳도 유심히 보자 | |
------- 유용한 에셋 ---------- | |
2D 게임 | |
Bolt | |
DoTween Pro | |
2DxFx 2D Sprite Fx | |
Odin - INspector and Serializer | |
Universal Sound Fx | |
Toony Colors Pro 2 | |
NGUI Next-Gen UI | |
Pro Camera 2D - The definitive 2D & 2.5D Unity camera plugin | |
인터페이스 및 항목 사운드 | |
Puppet2D | |
UI & Item Sound Effect Jingles | |
Fun Casual Sounds | |
Unity Anima2D | |
Chronos | |
Rainbow Folders | |
SpriteToPatitcles | |
Friendly & Clean UI | |
Chiptunes - Music & SFX Pack | |
Dialoguer | |
--------기본 폴더 구조------ | |
Data | |
ㄴStatic (게임 내에서 Resources.Load 로 호출되지 않고 AssetBundle에 묶이지 않는 모든 리소스는 여기에 넣습니다.) | |
ㄴResources (동적으로 로딩할 수 있는 모든 리소스가 포함됩니다.) | |
ㄴPatchData (AssetBundle 을 빌드하기 위해서 사용하고 로컬에서도 디버그 모드에선 AssetDataBase로 동작할 수 있게 작업합니다.) | |
-------- Static 폴더 그룹화 Rainbow Folders 에셋은 유룐데 GitProj 무료인거같음 설치하면 폴더 Types 바꾸면 아이콘하고 색 바뀐다. 이쁨 ----------- | |
01.Scenes | |
02.Prefabs | |
// 03.Bolt 만약 플러그인 안쓰면 버려도 됨 | |
03.Codes | |
04.Sprites | |
05.Fonts | |
06.Sounds | |
-------- Particle System Using Sprite Image 파티클 시스템 스프라이트 이미지 가지고 만들기 -------- | |
Material 하나 만든다음 Shader> Particles> Alpha Blended 누른다음 Particle Texture에 원하는 이미지 넣고 | |
Particle System 만들어서 Texture Sheet Animation 옵션 추가하고 Mode>Sprites 바꾸고 원하는 Sprite 넣고, | |
Renderer옵션에서 Material 만든 걸로 넣으면 된다. | |
-------- 파티클 모듈 스크립트로 제어하는 법 --------------- | |
var a = Instantiate(CoinAnim, new Vector3(3.64f,-3.61f,0f),Quaternion.identity) as ParticleSystem; | |
ParticleSystem.ShapeModule aa = a.shape; | |
aa.rotation = new Vector3(300f, 45f, 0f); | |
핵심은 ParticleSystem.ShapeModule 이런식으로 원하는 모듈을 변수에 담아서 그 하위 옵션들에 접근해 수정해야한다. | |
--------- 3개 이상 자료형 묶고 싶을 때 사용하는 자료구조 ------------ | |
유니티 2018.3 이전에는 c# 4.0기본인데 그 이후부턴 7.0 가능 | |
Find your .csproj file. | |
Change LangVersion property to desired version. | |
C# 7.0 이상부터 지원하는 문법 | |
vscode이면 .csproj 파일 가서 <LangVersion> 6 </LangVersion> 로 변경해도 되는데 에러뜰때는 player세팅 > other settings > .Net 3.x -> .Net 4.x 선택 | |
자동으로 .csproj <LangVersion>4 </LangVersion> -> 6으로 변경됨. | |
---- 방법 1 | |
using System; | |
using System.Collections.Generic; | |
new Tuple<GameObject, vector2, ContactPoint2D>(a,b,c) 이런식으로 쓸 수 있다. | |
.Item1 | |
---- 방법 2 | |
using System; | |
List<(Commands, int, float)> commandList = new List<(Commands, int, float)>(); | |
사용 할때는 commandList.Item1 .Item2 .Item3 ... 이런식으로 사용할 수 있다. | |
-------- 광고 -------- | |
광고는 현재(2018년 기준) | |
동영상광고, 배너광고 기준으로 봤을때 Unity Ads보다 Google ADMob이 돈이 더 된다. | |
애드몹 걍 | |
-------- 구글 플레이 연동 --------- | |
http://minhyeokism.tistory.com/70 | |
-------- 버튼 에니메이션 에니메이터 안에 애니메이션 만드는 방법 -------- | |
Transition > Animation 으로 변경하면 Auto Generate Animator 버튼이 생기고 그거 누르면 4가지 에니메이션이 삽입된 애니메이터가 생성된다. | |
이거 개꿀 ㅎㅎ | |
-------- 모바일 테스트 ------------ | |
0) 설정 > 개발자모드 > 디버깅 모드 실행 | |
1) 폰에서 Unity Remote5 를 실행 | |
2) 장치를 USB로 연결 | |
3) 유니티 실행 (켜져있었다면 재실행을 해야합니다.) | |
<Edit - Project Setting - Editor> 항목에서 Unity Remote를 Any Android Device로 선택되어 있어야함. | |
4) 게임 Paly | |
---------- 소수점 들어간 값 비교 시 (Float값 비교) ------------- | |
은근 특정 소수점 수치와 유사한지 비교해야 할 때가 있는데 이 때 C#의 소수점은 매우 부정확하므로 문제가 생길 수 있다. | |
Mathf.Approximately(float a, float b) 이 때 이걸 사용하면 유사값 판단해서 == 대신 쓸 수 있다. | |
-------- 씬 이름 가져오기----------- | |
Application.loadedLevelName | |
n.ToString("D2") // 01 02 패턴으로 | |
-------- 사운드는 걍 DestoryLoad로 AudioManager로 처리하자 ------- | |
사운드 매니저를 사용해야하는 가장 큰 이유!! - 옵션에서 효과음 끄는거 일일히 조건문으로 검색하고있을래? | |
그게 싫다면 매니저를 사용하자! | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Audio; | |
/// <summary> | |
/// 사운드 매니저 v1.0 made by JJSmith | |
/// </summary> | |
[RequireComponent (typeof (AudioSource))] | |
public class AudioManager : MonoBehaviour { | |
[Header ("배경음 설정")] | |
[Tooltip ("배경음 On/Off")] | |
[SerializeField] bool _musicOn = true; | |
[Tooltip ("배경음 볼륨")] | |
[Range (0, 1)] | |
[SerializeField] float _musicVolume = 1f; | |
[Tooltip ("시작 시 배경음 사용여부")] | |
[SerializeField] bool _useMusicVolOnStart = false; | |
[Tooltip ("Target Group 배경음 신호를 위한 설정, 사용 안 할경우 비워놓으면 됨.")] | |
[SerializeField] AudioMixerGroup _musicMixerGroup = null; | |
[Tooltip ("배경음 볼륨믹서 명")] | |
[SerializeField] string _volumeOfMusicMixer = string.Empty; | |
[Space (3)] | |
[Header ("효과음 설정")] | |
[Tooltip ("효과음 On/Off")] | |
[SerializeField] bool _soundFxOn = true; | |
[Tooltip ("효과음 볼륨")] | |
[Range (0, 1)] | |
[SerializeField] float _soundFxVolume = 1f; | |
[Tooltip ("시작 시 효과음 사용여부")] | |
[SerializeField] bool _useSfxVolOnStart = false; | |
[Tooltip ("Target Group 효과음 신호를 위한 설정, 사용 안 할경우 비워놓으면 됨.")] | |
[SerializeField] AudioMixerGroup _soundFxMixerGroup = null; | |
[Tooltip ("효과음 볼륨믹서 명")] | |
[SerializeField] string _volumeOfSFXMixer = string.Empty; | |
[Space (3)] | |
[Tooltip ("모든 오디오 클립은 여기에 넣으면 됨.")] | |
[SerializeField] List<AudioClip> _playlist = new List<AudioClip> (); | |
// 효과음 풀링을 위한 리스트 | |
List<SoundEffect> sfxPool = new List<SoundEffect> (); | |
// 오디오 매니저 배경음 | |
static BackgroundMusic backgroundMusic; | |
// 현재 오디오소스와 페이드를 위한 다음 오디오소스 | |
static AudioSource musicSource = null, crossfadeSource = null; | |
// 현재 볼륨들과 제한 수치용 변수 | |
static float currentMusicVol = 0, currentSfxVol = 0, musicVolCap = 0, savedPitch = 1f; | |
// On/Off 변수 | |
static bool musicOn = false, sfxOn = false; | |
// 전환시간 변수 | |
static float transitionTime; | |
// PlayerPrefabs 저장을 위한 키 | |
static readonly string BgMusicVolKey = "BGMVol"; | |
static readonly string SoundFxVolKey = "SFXVol"; | |
static readonly string BgMusicMuteKey = "BGMMute"; | |
static readonly string SoundFxMuteKey = "SFXMute"; | |
// 유일한 인스턴스 변수 | |
private static AudioManager inst; | |
// 앱 켜졌는지 여부용 | |
private static bool alive = true; | |
/// <summary> | |
/// 속성 싱글톤 패턴으로 구현 | |
/// </summary> | |
public static AudioManager Instance { | |
get { | |
// 앱이 꺼젔거나 Destroy됬는지 체크 | |
if (!alive) { | |
Debug.LogWarning (typeof (AudioManager) + "' is already destroyed on application quit."); | |
return null; | |
} | |
//C# 2.0 Null 병합연산자 | |
return inst ?? FindObjectOfType<AudioManager> (); | |
} | |
} | |
void OnDestroy () { | |
StopAllCoroutines (); | |
SaveAllPreferences (); | |
} | |
void OnApplicationQuit () { | |
alive = false; | |
} | |
/// <summary> | |
/// 오디오매니저 초기화 함수 | |
/// </summary> | |
void Initialise () { | |
gameObject.name = "AudioManager"; | |
// PlayerPrefs에서 값 가져오기 | |
_musicOn = LoadBGMMuteStatus (); | |
_musicVolume = _useMusicVolOnStart ? _musicVolume : LoadBGMVolume (); | |
_soundFxOn = LoadSFXMuteStatus (); | |
_soundFxVolume = _useSfxVolOnStart ? _soundFxVolume : LoadSFXVolume (); | |
// 기존 오디오소스 컴포넌트 장착 | |
if (musicSource == null) { | |
musicSource = gameObject.GetComponent<AudioSource> (); | |
// 오디오소스 컴포넌트 없으면 생성해서 부착 | |
musicSource = musicSource ?? gameObject.AddComponent<AudioSource> (); | |
} | |
musicSource = ConfigureAudioSource (musicSource); | |
// 씬 전환시에도 파괴되지 않도록 설정 | |
DontDestroyOnLoad (this.gameObject); | |
} | |
void Awake () { | |
if (inst == null) { | |
inst = this; | |
Initialise (); | |
} else if (inst != this) { | |
Destroy (this.gameObject); | |
} | |
} | |
void Start () { | |
if (musicSource != null) { | |
StartCoroutine (OnUpdate ()); | |
} | |
} | |
/// <summary> | |
/// 내부 설정에 기반해서 2D용 오디오소스 생성하는 함수 | |
/// </summary> | |
/// <returns>An AudioSource with 2D features</returns> | |
AudioSource ConfigureAudioSource (AudioSource audioSource) { | |
audioSource.outputAudioMixerGroup = _musicMixerGroup; | |
audioSource.playOnAwake = false; | |
audioSource.spatialBlend = 0; //2D | |
audioSource.rolloffMode = AudioRolloffMode.Linear; | |
audioSource.loop = true; | |
// PlayerPrefs에서 값 가져오기 | |
audioSource.volume = LoadBGMVolume (); | |
audioSource.mute = !_musicOn; | |
return audioSource; | |
} | |
/// <summary> | |
/// 효과음 풀에 있는 효과음을 관리하는 함수 | |
/// OnUpdate함수에서 불러온다. | |
/// </summary> | |
private void ManageSoundEffects () { | |
for (int i = sfxPool.Count - 1; i >= 0; i--) { | |
SoundEffect sfx = sfxPool[i]; | |
// 재생 중 | |
if (sfx.Source.isPlaying && !float.IsPositiveInfinity (sfx.Time)) { | |
sfx.Time -= Time.deltaTime; | |
sfxPool[i] = sfx; | |
} | |
// 끝났을 때 | |
if (sfxPool[i].Time <= 0.0001f || HasPossiblyFinished (sfxPool[i])) { | |
sfxPool[i].Source.Stop (); | |
// 콜백함수 실행 | |
if (sfxPool[i].Callback != null) { | |
sfxPool[i].Callback.Invoke (); | |
} | |
// 클립 제거 후 | |
Destroy (sfxPool[i].gameObject); | |
// 풀에서 항목빼기 | |
sfxPool.RemoveAt (i); | |
break; | |
} | |
} | |
} | |
// 완전히 끝났는 지 체크용 함수 | |
bool HasPossiblyFinished (SoundEffect soundEffect) { | |
return !soundEffect.Source.isPlaying && FloatEquals (soundEffect.PlaybackPosition, 0) && soundEffect.Time <= 0.09f; | |
} | |
bool FloatEquals (float num1, float num2, float threshold = .0001f) { | |
return Math.Abs (num1 - num2) < threshold; | |
} | |
/// <summary> | |
/// 배경음 볼륨 상태가 변했는지 체크하는 함수 | |
/// </summary> | |
private bool IsMusicAltered () { | |
bool flag = musicOn != _musicOn || musicOn != !musicSource.mute || !FloatEquals (currentMusicVol, _musicVolume); | |
// 믹서 그룹을 사용할 경우 | |
if (_musicMixerGroup != null && !string.IsNullOrEmpty (_volumeOfMusicMixer.Trim ())) { | |
float vol; | |
_musicMixerGroup.audioMixer.GetFloat (_volumeOfMusicMixer, out vol); | |
vol = NormaliseVolume (vol); | |
return flag || !FloatEquals (currentMusicVol, vol); | |
} | |
return flag; | |
} | |
/// <summary> | |
/// 효과음 볼륨 상태가 변했는지 체크하는 함수 | |
/// </summary> | |
private bool IsSoundFxAltered () { | |
bool flag = _soundFxOn != sfxOn || !FloatEquals (currentSfxVol, _soundFxVolume); | |
// 믹서 그룹을 사용할 경우 | |
if (_soundFxMixerGroup != null && !string.IsNullOrEmpty (_volumeOfSFXMixer.Trim ())) { | |
float vol; | |
_soundFxMixerGroup.audioMixer.GetFloat (_volumeOfSFXMixer, out vol); | |
vol = NormaliseVolume (vol); | |
return flag || !FloatEquals (currentSfxVol, vol); | |
} | |
return flag; | |
} | |
/// <summary> | |
/// 크로스 페이드 인 아웃 함수 | |
/// </summary> | |
private void CrossFadeBackgroundMusic () { | |
if (backgroundMusic.MusicTransition == MusicTransition.CrossFade) { | |
// 전환이 진행중일 경우 | |
if (musicSource.clip.name != backgroundMusic.NextClip.name) { | |
transitionTime -= Time.deltaTime; | |
musicSource.volume = Mathf.Lerp (0, musicVolCap, transitionTime / backgroundMusic.TransitionDuration); | |
crossfadeSource.volume = Mathf.Clamp01 (musicVolCap - musicSource.volume); | |
crossfadeSource.mute = musicSource.mute; | |
if (musicSource.volume <= 0.00f) { | |
SetBGMVolume (musicVolCap); | |
PlayBackgroundMusic (backgroundMusic.NextClip, crossfadeSource.time, crossfadeSource.pitch); | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// 페이드 인/아웃 함수 | |
/// </summary> | |
private void FadeOutFadeInBackgroundMusic () { | |
if (backgroundMusic.MusicTransition == MusicTransition.LinearFade) { | |
// 페이드 인 | |
if (musicSource.clip.name == backgroundMusic.NextClip.name) { | |
transitionTime += Time.deltaTime; | |
musicSource.volume = Mathf.Lerp (0, musicVolCap, transitionTime / backgroundMusic.TransitionDuration); | |
if (musicSource.volume >= musicVolCap) { | |
SetBGMVolume (musicVolCap); | |
PlayBackgroundMusic (backgroundMusic.NextClip, musicSource.time, savedPitch); | |
} | |
} | |
// 페이드 아웃 | |
else { | |
transitionTime -= Time.deltaTime; | |
musicSource.volume = Mathf.Lerp (0, musicVolCap, transitionTime / backgroundMusic.TransitionDuration); | |
// 페이드 아웃 끝나는 시점 페이드 인 시작 | |
if (musicSource.volume <= 0.00f) { | |
musicSource.volume = transitionTime = 0; | |
PlayMusicFromSource (ref musicSource, backgroundMusic.NextClip, 0, musicSource.pitch); | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// 업데이트 함수 용 Enumerator | |
/// </summary> | |
IEnumerator OnUpdate () { | |
while (alive) { | |
ManageSoundEffects (); | |
// 배경음 볼륨 바뀌었나 체크 | |
if (IsMusicAltered ()) { | |
ToggleBGMMute (!musicOn); | |
if (!FloatEquals (currentMusicVol, _musicVolume)) { | |
currentMusicVol = _musicVolume; | |
} | |
if (_musicMixerGroup != null && !string.IsNullOrEmpty (_volumeOfMusicMixer)) { | |
float vol; | |
_musicMixerGroup.audioMixer.GetFloat (_volumeOfMusicMixer, out vol); | |
vol = NormaliseVolume (vol); | |
currentMusicVol = vol; | |
} | |
SetBGMVolume (currentMusicVol); | |
} | |
// 효과음 볼륨 바뀌었나 체크 | |
if (IsSoundFxAltered ()) { | |
ToggleSFXMute (!sfxOn); | |
if (!FloatEquals (currentSfxVol, _soundFxVolume)) { | |
currentSfxVol = _soundFxVolume; | |
} | |
if (_soundFxMixerGroup != null && !string.IsNullOrEmpty (_volumeOfSFXMixer)) { | |
float vol; | |
_soundFxMixerGroup.audioMixer.GetFloat (_volumeOfSFXMixer, out vol); | |
vol = NormaliseVolume (vol); | |
currentSfxVol = vol; | |
} | |
SetSFXVolume (currentSfxVol); | |
} | |
// 크로스 페이드일 경우 | |
if (crossfadeSource != null) { | |
CrossFadeBackgroundMusic (); | |
yield return null; | |
} else { | |
// 페이드 인/ 아웃일 경우 | |
if (backgroundMusic.NextClip != null) { | |
FadeOutFadeInBackgroundMusic (); | |
yield return null; | |
} | |
} | |
yield return new WaitForEndOfFrame (); | |
} | |
} | |
/// <summary> | |
/// 특정한 오디오소스에서 클립을 재생하는 함수 | |
/// </summary> | |
/// <param name="audio_source">참조하는 오디오소스/ 채널</param> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="playback_position">시작시점</param> | |
/// <param name="pitch">클립의 피치 레벨 설정</param> | |
private void PlayMusicFromSource (ref AudioSource audio_source, AudioClip clip, float playback_position, float pitch) { | |
try { | |
audio_source.clip = clip; | |
audio_source.time = playback_position; | |
audio_source.pitch = pitch = Mathf.Clamp (pitch, -3f, 3f); | |
audio_source.Play (); | |
} catch (NullReferenceException nre) { | |
Debug.LogError (nre.Message); | |
} catch (Exception e) { | |
Debug.LogError (e.Message); | |
} | |
} | |
/// <summary> | |
/// 현재 오디오소스에서 클립을 재생하는 함수 | |
/// </summary> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="playback_position">시작시점</param> | |
/// <param name="pitch">클립의 피치 레벨 설정</param> | |
private void PlayBackgroundMusic (AudioClip clip, float playback_position, float pitch) { | |
PlayMusicFromSource (ref musicSource, clip, playback_position, pitch); | |
// 다음 클립변수에 있는 클립 제거 | |
backgroundMusic.NextClip = null; | |
// 현재 클립변수에 넣어두기 | |
backgroundMusic.CurrentClip = clip; | |
// 크로스페이드에 있는 클립도 비우기 | |
if (crossfadeSource != null) { | |
Destroy (crossfadeSource); | |
crossfadeSource = null; | |
} | |
} | |
/// <summary> | |
/// 배경음 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="transition">전환방법 </param> | |
/// <param name="transition_duration">전환시간</param> | |
/// <param name="volume">사운드 크기</param> | |
/// <param name="pitch">클립의 피치 레벨 설정</param> | |
/// <param name="playback_position">시작시점</param> | |
public void PlayBGM (AudioClip clip, MusicTransition transition, float transition_duration, float volume, float pitch, float playback_position = 0) { | |
// 요구클립이 없거나 똑같은 클립이면 재생하지 않음. | |
if (clip == null || backgroundMusic.CurrentClip == clip) { | |
return; | |
} | |
// 첫 번째로 플레이한 음악이거나 전환시간이 0이면 - 전환효과 없는 케이스 | |
if (backgroundMusic.CurrentClip == null || transition_duration <= 0) { | |
transition = MusicTransition.Swift; | |
} | |
// 전환효과 없는 케이스 시작 | |
if (transition == MusicTransition.Swift) { | |
PlayBackgroundMusic (clip, playback_position, pitch); | |
SetBGMVolume (volume); | |
} else { | |
// 전환효과 진행중일 때 막음 | |
if (backgroundMusic.NextClip != null) { | |
Debug.LogWarning ("Trying to perform a transition on the background music while one is still active"); | |
return; | |
} | |
// 전환효과 변수에 전환방법대로 지정, 그 외 변수들도.. | |
backgroundMusic.MusicTransition = transition; | |
transitionTime = backgroundMusic.TransitionDuration = transition_duration; | |
musicVolCap = _musicVolume; | |
backgroundMusic.NextClip = clip; | |
// 크로스페이드 처리 | |
if (backgroundMusic.MusicTransition == MusicTransition.CrossFade) { | |
// 전환효과 진행중일 때 막음 | |
if (crossfadeSource != null) { | |
Debug.LogWarning ("Trying to perform a transition on the background music while one is still active"); | |
return; | |
} | |
// 크로스페이드 오디오 초기화 | |
crossfadeSource = ConfigureAudioSource (gameObject.AddComponent<AudioSource> ()); | |
crossfadeSource.volume = Mathf.Clamp01 (musicVolCap - currentMusicVol); | |
crossfadeSource.priority = 0; | |
PlayMusicFromSource (ref crossfadeSource, backgroundMusic.NextClip, 0, pitch); | |
} | |
} | |
} | |
/// <summary> | |
/// 배경음 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="transition">전환방법</param> | |
/// <param name="transition_duration">전환시간</param> | |
/// <param name="volume">사운드 크기</param> | |
public void PlayBGM (AudioClip clip, MusicTransition transition, float transition_duration, float volume) { | |
PlayBGM (clip, transition, transition_duration, volume, 1f); | |
} | |
/// <summary> | |
/// 배경음 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="transition">전환방법</param> | |
/// <param name="transition_duration">전환시간</param> | |
public void PlayBGM (AudioClip clip, MusicTransition transition, float transition_duration) { | |
PlayBGM (clip, transition, transition_duration, _musicVolume, 1f); | |
} | |
/// <summary> | |
/// 배경음 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="transition">전환방법</param> | |
public void PlayBGM (AudioClip clip, MusicTransition transition) { | |
PlayBGM (clip, transition, 1f, _musicVolume, 1f); | |
} | |
/// <summary> | |
/// 배경음 바로 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip">재생할 클립</param> | |
public void PlayBGM (AudioClip clip) { | |
PlayBGM (clip, MusicTransition.Swift, 1f, _musicVolume, 1f); | |
} | |
/// <summary> | |
/// 배경음 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip_path">Resources 폴더에 있는 클립 경로</param> | |
/// <param name="transition">전환방법 </param> | |
/// <param name="transition_duration">전환시간</param> | |
/// <param name="volume">사운드 크기</param> | |
/// <param name="pitch">클립의 피치 레벨 설정</param> | |
/// <param name="playback_position">시작시점</param> | |
public void PlayBGM (string clip_path, MusicTransition transition, float transition_duration, float volume, float pitch, float playback_position = 0) { | |
PlayBGM (LoadClip (clip_path), transition, transition_duration, volume, pitch, playback_position); | |
} | |
/// <summary> | |
/// 배경음 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip_path">Resources 폴더에 있는 클립 경로</param> | |
/// <param name="transition">전환방법 </param> | |
/// <param name="transition_duration">전환시간</param> | |
/// <param name="volume">사운드 크기</param> | |
public void PlayBGM (string clip_path, MusicTransition transition, float transition_duration, float volume) { | |
PlayBGM (LoadClip (clip_path), transition, transition_duration, volume, 1f); | |
} | |
/// <summary> | |
/// 배경음 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip_path">Resources 폴더에 있는 클립 경로</param> | |
/// <param name="transition">전환방법 </param> | |
/// <param name="transition_duration">전환시간</param> | |
public void PlayBGM (string clip_path, MusicTransition transition, float transition_duration) { | |
PlayBGM (LoadClip (clip_path), transition, transition_duration, _musicVolume, 1f); | |
} | |
/// <summary> | |
/// 배경음 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip_path">Resources 폴더에 있는 클립 경로</param> | |
/// <param name="transition">전환방법 </param> | |
public void PlayBGM (string clip_path, MusicTransition transition) { | |
PlayBGM (LoadClip (clip_path), transition, 1f, _musicVolume, 1f); | |
} | |
/// <summary> | |
/// 배경음 바로 재생 | |
/// 배경음은 한 번에 한 개만 재생. | |
/// </summary> | |
/// <param name="clip_path">Resources 폴더에 있는 클립 경로</param> | |
public void PlayBGM (string clip_path) { | |
PlayBGM (LoadClip (clip_path), MusicTransition.Swift, 1f, _musicVolume, 1f); | |
} | |
/// <summary> | |
/// 배경음 중지 | |
/// </summary> | |
public void StopBGM () { | |
if (musicSource.isPlaying) { | |
musicSource.Stop (); | |
} | |
} | |
/// <summary> | |
/// 배경음 일시정지 | |
/// </summary> | |
public void PauseBGM () { | |
if (musicSource.isPlaying) { | |
musicSource.Pause (); | |
} | |
} | |
/// <summary> | |
/// 배경음 다시재생 | |
/// </summary> | |
public void ResumeBGM () { | |
if (!musicSource.isPlaying) { | |
musicSource.UnPause (); | |
} | |
} | |
/// <summary> | |
/// 모든 효과음에서 사용되는 내장 기본함수 | |
/// 효과음에 대한 특정 항목을 초기화함. | |
/// </summary> | |
/// <param name="audio_clip">재생할 클립</param> | |
/// <param name="location">클립의 생성 위치 (2D)</param> | |
/// <returns>Newly created gameobject with sound effect and audio source attached</returns> | |
private GameObject CreateSoundFx (AudioClip audio_clip, Vector2 location) { | |
// 임시 오브젝트 | |
GameObject host = new GameObject ("TempAudio"); | |
host.transform.position = location; | |
host.transform.SetParent (transform); | |
host.AddComponent<SoundEffect> (); | |
// 오디오소스 추가 | |
AudioSource audioSource = host.AddComponent<AudioSource> () as AudioSource; | |
audioSource.playOnAwake = false; | |
audioSource.spatialBlend = 0; | |
audioSource.rolloffMode = AudioRolloffMode.Logarithmic; | |
// 믹서 그룹을 사용할 경우 | |
audioSource.outputAudioMixerGroup = _soundFxMixerGroup; | |
audioSource.clip = audio_clip; | |
audioSource.mute = !_soundFxOn; | |
return host; | |
} | |
/// <summary> | |
/// 효과음이 효과음 풀에 존재하면 인덱스 알려주는 함수 | |
/// </summary> | |
/// <param name="name">효과음 이름</param> | |
/// <param name="singleton">효과음이 싱글톤인지 여부</param> | |
/// <returns>Index of sound effect or -1 is none exists</returns> | |
public int IndexOfSoundFxPool (string name, bool singleton = false) { | |
int index = 0; | |
while (index < sfxPool.Count) { | |
if (sfxPool[index].Name == name && singleton == sfxPool[index].Singleton) { | |
return index; | |
} | |
index++; | |
} | |
return -1; | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 지정된 시간만큼 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An audiosource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="location">클립의 생성 위치 (2D)</param> | |
/// <param name="duration">재생시간</param> | |
/// <param name="volume">사운드 크기</param> | |
/// <param name="singleton">효과음이 싱글톤인지 여부</param> | |
/// <param name="pitch">클립의 피치 레벨 설정</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource PlaySFX (AudioClip clip, Vector2 location, float duration, float volume, bool singleton = false, float pitch = 1f, Action callback = null) { | |
if (duration <= 0 || clip == null) { | |
return null; | |
} | |
int index = IndexOfSoundFxPool (clip.name, true); | |
if (index >= 0) { | |
// 효과음 풀에 존재하면 재생시간 재설정해서 내보냄 | |
SoundEffect singletonSFx = sfxPool[index]; | |
singletonSFx.Duration = singletonSFx.Time = duration; | |
sfxPool[index] = singletonSFx; | |
return sfxPool[index].Source; | |
} | |
GameObject host = null; | |
AudioSource source = null; | |
host = CreateSoundFx (clip, location); | |
source = host.GetComponent<AudioSource> (); | |
source.loop = duration > clip.length; | |
source.volume = _soundFxVolume * volume; | |
source.pitch = pitch; | |
// 재사용 가능한 사운드 생성 | |
SoundEffect sfx = host.GetComponent<SoundEffect> (); | |
sfx.Singleton = singleton; | |
sfx.Source = source; | |
sfx.OriginalVolume = volume; | |
sfx.Duration = sfx.Time = duration; | |
sfx.Callback = callback; | |
// 풀에 넣는다. | |
sfxPool.Add (sfx); | |
source.Play (); | |
return source; | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 지정된 시간만큼 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An audiosource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="location">클립의 생성 위치 (2D)</param> | |
/// <param name="duration">재생시간</param> | |
/// <param name="singleton">효과음이 싱글톤인지 여부</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource PlaySFX (AudioClip clip, Vector2 location, float duration, bool singleton = false, Action callback = null) { | |
return PlaySFX (clip, location, duration, _soundFxVolume, singleton, 1f, callback); | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 지정된 시간만큼 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An audiosource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="duration">재생시간</param> | |
/// <param name="singleton">효과음이 싱글톤인지 여부</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource PlaySFX (AudioClip clip, float duration, bool singleton = false, Action callback = null) { | |
return PlaySFX (clip, Vector2.zero, duration, _soundFxVolume, singleton, 1f, callback); | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 지정된 횟수만큼 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An audiosource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="location">클립의 생성 위치 (2D)</param> | |
/// <param name="repeat">클립을 얼마나 반복할지 정한다. 무한은 음수를 입력하면 됨.</param> | |
/// <param name="volume">사운드 크기</param> | |
/// <param name="singleton">효과음이 싱글톤인지 여부</param> | |
/// <param name="pitch">클립의 피치 레벨 설정</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource RepeatSFX (AudioClip clip, Vector2 location, int repeat, float volume, bool singleton = false, float pitch = 1f, Action callback = null) { | |
if (clip == null) { | |
return null; | |
} | |
if (repeat != 0) { | |
int index = IndexOfSoundFxPool (clip.name, true); | |
if (index >= 0) { | |
// 효과음 풀에 존재하면 재생시간 재설정해서 내보냄 | |
SoundEffect singletonSFx = sfxPool[index]; | |
singletonSFx.Duration = singletonSFx.Time = repeat > 0 ? clip.length * repeat : float.PositiveInfinity; | |
sfxPool[index] = singletonSFx; | |
return sfxPool[index].Source; | |
} | |
GameObject host = CreateSoundFx (clip, location); | |
AudioSource source = host.GetComponent<AudioSource> (); | |
source.loop = repeat != 0; | |
source.volume = _soundFxVolume * volume; | |
source.pitch = pitch; | |
// 재사용 가능한 사운드 생성 | |
SoundEffect sfx = host.GetComponent<SoundEffect> (); | |
sfx.Singleton = singleton; | |
sfx.Source = source; | |
sfx.OriginalVolume = volume; | |
sfx.Duration = sfx.Time = repeat > 0 ? clip.length * repeat : float.PositiveInfinity; | |
sfx.Callback = callback; | |
// 풀에 넣는다. | |
sfxPool.Add (sfx); | |
source.Play (); | |
return source; | |
} | |
// repeat 길이가 1보다 작거나 같으면 재생 | |
return PlayOneShot (clip, location, volume, pitch, callback); | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 지정된 횟수만큼 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An audiosource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="location">클립의 생성 위치 (2D)</param> | |
/// <param name="repeat">클립을 얼마나 반복할지 정한다. 무한은 음수를 입력하면 됨.</param> | |
/// <param name="singleton">효과음이 싱글톤인지 여부</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource RepeatSFX (AudioClip clip, Vector2 location, int repeat, bool singleton = false, Action callback = null) { | |
return RepeatSFX (clip, location, repeat, _soundFxVolume, singleton, 1f, callback); | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 지정된 횟수만큼 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An audiosource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="repeat">클립을 얼마나 반복할지 정한다. 무한은 음수를 입력하면 됨.</param> | |
/// <param name="singleton">효과음이 싱글톤인지 여부</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource RepeatSFX (AudioClip clip, int repeat, bool singleton = false, Action callback = null) { | |
return RepeatSFX (clip, Vector2.zero, repeat, _soundFxVolume, singleton, 1f, callback); | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An AudioSource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="location">클립의 생성 위치 (2D)</param> | |
/// <param name="volume">사운드 크기</param> | |
/// <param name="pitch">클립의 피치 레벨 설정</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource PlayOneShot (AudioClip clip, Vector2 location, float volume, float pitch = 1f, Action callback = null) { | |
if (clip == null) { | |
return null; | |
} | |
GameObject host = CreateSoundFx (clip, location); | |
AudioSource source = host.GetComponent<AudioSource> (); | |
source.loop = false; | |
source.volume = _soundFxVolume * volume; | |
source.pitch = pitch; | |
// 재사용 가능한 사운드 생성 | |
SoundEffect sfx = host.GetComponent<SoundEffect> (); | |
sfx.Singleton = false; | |
sfx.Source = source; | |
sfx.OriginalVolume = volume; | |
sfx.Duration = sfx.Time = clip.length; | |
sfx.Callback = callback; | |
// 풀에 넣는다. | |
sfxPool.Add (sfx); | |
source.Play (); | |
return source; | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An AudioSource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="location">클립의 생성 위치 (2D)</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource PlayOneShot (AudioClip clip, Vector2 location, Action callback = null) { | |
return PlayOneShot (clip, location, _soundFxVolume, 1f, callback); | |
} | |
/// <summary> | |
/// 월드 스페이스(2D)에서 효과음을 재생하고 끝나면 지정된 콜백 함수를 호출하는 함수 | |
/// </summary> | |
/// <returns>An AudioSource</returns> | |
/// <param name="clip">재생할 클립</param> | |
/// <param name="callback">재생이 끝나면 콜백할 액션</param> | |
public AudioSource PlayOneShot (AudioClip clip, Action callback = null) { | |
return PlayOneShot (clip, Vector2.zero, _soundFxVolume, 1f, callback); | |
} | |
/// <summary> | |
/// 모든 효과음을 일시정지 | |
/// </summary> | |
public void PauseAllSFX () { | |
// SoundEffect 다 돌기 | |
foreach (SoundEffect sfx in FindObjectsOfType<SoundEffect> ()) { | |
if (sfx.Source.isPlaying) sfx.Source.Pause (); | |
} | |
} | |
/// <summary> | |
/// 모든 효과음을 다시재생 | |
/// </summary> | |
public void ResumeAllSFX () { | |
foreach (SoundEffect sfx in FindObjectsOfType<SoundEffect> ()) { | |
if (!sfx.Source.isPlaying) sfx.Source.UnPause (); | |
} | |
} | |
/// <summary> | |
/// 모든 효과음을 중지 | |
/// </summary> | |
public void StopAllSFX () { | |
foreach (SoundEffect sfx in FindObjectsOfType<SoundEffect> ()) { | |
if (sfx.Source) { | |
sfx.Source.Stop (); | |
Destroy (sfx.gameObject); | |
} | |
} | |
sfxPool.Clear (); | |
} | |
/// <summary> | |
/// Resources 폴더에서 오디오 클립을 가져오는 함수 | |
/// </summary> | |
/// <param name="path">Resources 폴더의 클립 경로</param> | |
/// <param name="add_to_playlist">로드한 클립을 나중에 참조를 위해서 플레이 리스트에 추가하는 옵션</param> | |
/// <returns>The Audioclip from the resource folder</returns> | |
public AudioClip LoadClip (string path, bool add_to_playlist = false) { | |
AudioClip clip = Resources.Load (path) as AudioClip; | |
if (clip == null) { | |
Debug.LogError (string.Format ("AudioClip '{0}' not found at location {1}", path, System.IO.Path.Combine (Application.dataPath, "/Resources/" + path))); | |
return null; | |
} | |
if (add_to_playlist) { | |
AddToPlaylist (clip); | |
} | |
return clip; | |
} | |
/// <summary> | |
/// URL 경로로 오디오 클립을 가져오는 함수 | |
/// </summary> | |
/// <param name="path">오디오 클립 다운로드 URL. 예: 'http://www.my-server.com/audio.ogg'</param> | |
/// <param name="audio_type">다운로드를 위한 오디오 인코딩 타입. AudioType 참고</param> | |
/// <param name="add_to_playlist">로드한 클립을 나중에 참조를 위해서 플레이 리스트에 추가하는 옵션</param> | |
/// <param name="callback">로드가 완료되면 콜백할 액션.</param> | |
public void LoadClip (string path, AudioType audio_type, bool add_to_playlist, Action<AudioClip> callback) { | |
StartCoroutine (LoadAudioClipFromUrl (path, audio_type, (downloadedContent) => { | |
if (downloadedContent != null && add_to_playlist) { | |
AddToPlaylist (downloadedContent); | |
} | |
callback.Invoke (downloadedContent); | |
})); | |
} | |
/// <summary> | |
/// URL 경로로 오디오 클립 가져오는 내장 함수 | |
/// </summary> | |
/// <returns>The audio clip from URL.</returns> | |
/// <param name="audio_url">오디오 URL</param> | |
/// <param name="audio_type">오디오 타입</param> | |
/// <param name="callback">콜백 액션</param> | |
IEnumerator LoadAudioClipFromUrl (string audio_url, AudioType audio_type, Action<AudioClip> callback) { | |
using (UnityEngine.Networking.UnityWebRequest www = UnityEngine.Networking.UnityWebRequestMultimedia.GetAudioClip (audio_url, audio_type)) { | |
yield return www.SendWebRequest (); | |
if (www.isNetworkError) { | |
Debug.Log (string.Format ("Error downloading audio clip at {0} : {1}", audio_url, www.error)); | |
} | |
callback.Invoke (UnityEngine.Networking.DownloadHandlerAudioClip.GetContent (www)); | |
} | |
} | |
/// <summary> | |
/// 배경음, 효과음 On/Off 토글 함수 | |
/// </summary> | |
/// <param name="flag">On - true, Off - false</param> | |
private void ToggleMute (bool flag) { | |
ToggleBGMMute (flag); | |
ToggleSFXMute (flag); | |
} | |
/// <summary> | |
/// 배경음 On/Off 토글 함수 | |
/// </summary> | |
/// <param name="flag">On - true, Off - false</param> | |
private void ToggleBGMMute (bool flag) { | |
musicOn = _musicOn = flag; | |
musicSource.mute = !musicOn; | |
} | |
/// <summary> | |
/// 효과음 On/Off 토글 함수 | |
/// </summary> | |
/// <param name="flag">On - true, Off - false</param> | |
private void ToggleSFXMute (bool flag) { | |
sfxOn = _soundFxOn = flag; | |
foreach (SoundEffect sfx in FindObjectsOfType<SoundEffect> ()) { | |
sfx.Source.mute = !sfxOn; | |
} | |
} | |
/// <summary> | |
/// 배경음 사운드 크기조정 함수 | |
/// </summary> | |
/// <param name="volume">New volume of the background music.</param> | |
private void SetBGMVolume (float volume) { | |
try { | |
volume = Mathf.Clamp01 (volume); | |
// 모든 사운드 크기 변수에 할당 | |
musicSource.volume = currentMusicVol = _musicVolume = volume; | |
if (_musicMixerGroup != null && !string.IsNullOrEmpty (_volumeOfMusicMixer.Trim ())) { | |
float mixerVol = -80f + (volume * 100f); | |
_musicMixerGroup.audioMixer.SetFloat (_volumeOfMusicMixer, mixerVol); | |
} | |
} catch (NullReferenceException nre) { | |
Debug.LogError (nre.Message); | |
} catch (Exception e) { | |
Debug.LogError (e.Message); | |
} | |
} | |
/// <summary> | |
/// 효과음 사운드 크기조정 함수 | |
/// </summary> | |
/// <param name="volume">New volume for all the sound effects.</param> | |
private void SetSFXVolume (float volume) { | |
try { | |
volume = Mathf.Clamp01 (volume); | |
currentSfxVol = _soundFxVolume = volume; | |
foreach (SoundEffect sfx in FindObjectsOfType<SoundEffect> ()) { | |
sfx.Source.volume = _soundFxVolume * sfx.OriginalVolume; | |
sfx.Source.mute = !_soundFxOn; | |
} | |
if (_soundFxMixerGroup != null && !string.IsNullOrEmpty (_volumeOfSFXMixer.Trim ())) { | |
float mixerVol = -80f + (volume * 100f); | |
_soundFxMixerGroup.audioMixer.SetFloat (_volumeOfSFXMixer, mixerVol); | |
} | |
} catch (NullReferenceException nre) { | |
Debug.LogError (nre.Message); | |
} catch (Exception e) { | |
Debug.LogError (e.Message); | |
} | |
} | |
/// <summary> | |
/// 오디오 관리자 사운드 크기를 0- 1 로 정규화하는 함수 | |
/// </summary> | |
/// <returns>The normalised volume between the range of zero and one.</returns> | |
/// <param name="vol">사운드 크기</param> | |
private float NormaliseVolume (float vol) { | |
vol += 80f; | |
vol /= 100f; | |
return vol; | |
} | |
/// <summary> | |
/// 배경음 사운드 크기를 PlayerPrefs에서 가져오는 함수 | |
/// </summary> | |
/// <returns></returns> | |
private float LoadBGMVolume () { | |
return PlayerPrefs.HasKey (BgMusicVolKey) ? PlayerPrefs.GetFloat (BgMusicVolKey) : _musicVolume; | |
} | |
/// <summary> | |
/// 효과음 사운드 크기를 PlayerPrefs에서 가져오는 함수 | |
/// </summary> | |
/// <returns></returns> | |
private float LoadSFXVolume () { | |
return PlayerPrefs.HasKey (SoundFxVolKey) ? PlayerPrefs.GetFloat (SoundFxVolKey) : _soundFxVolume; | |
} | |
/// <summary> | |
/// int값을 bool값으로 변환하는 함수 | |
/// </summary> | |
private bool ToBool (int integer) { | |
return integer == 0 ? false : true; | |
} | |
/// <summary> | |
/// 배경음 On/Off 여부를 PlayerPrefs에서 가져오는 함수 | |
/// </summary> | |
/// <returns>Returns the value of the background music mute key from the saved preferences if it exists or the defaut value if it does not</returns> | |
private bool LoadBGMMuteStatus () { | |
return PlayerPrefs.HasKey (BgMusicMuteKey) ? ToBool (PlayerPrefs.GetInt (BgMusicMuteKey)) : _musicOn; | |
} | |
/// <summary> | |
/// 효과음 On/Off 여부를 PlayerPrefs에서 가져오는 함수 | |
/// </summary> | |
/// <returns>Returns the value of the sound effect mute key from the saved preferences if it exists or the defaut value if it does not</returns> | |
private bool LoadSFXMuteStatus () { | |
return PlayerPrefs.HasKey (SoundFxMuteKey) ? ToBool (PlayerPrefs.GetInt (SoundFxMuteKey)) : _soundFxOn; | |
} | |
/// <summary> | |
/// 배경음 On/Off 여부와 사운드 크기를 PlayerPrefs에 저장하는 함수 | |
/// </summary> | |
public void SaveBGMPreferences () { | |
PlayerPrefs.SetInt (BgMusicMuteKey, _musicOn ? 1 : 0); | |
PlayerPrefs.SetFloat (BgMusicVolKey, _musicVolume); | |
PlayerPrefs.Save (); | |
} | |
/// <summary> | |
/// 효과음 On/Off 여부와 사운드 크기를 PlayerPrefs에 저장하는 함수 | |
/// </summary> | |
public void SaveSFXPreferences () { | |
PlayerPrefs.SetInt (SoundFxMuteKey, _soundFxOn ? 1 : 0); | |
PlayerPrefs.SetFloat (SoundFxVolKey, _soundFxVolume); | |
PlayerPrefs.Save (); | |
} | |
/// <summary> | |
/// 모든 PlayerPrefs 초기화 하는 함수 | |
/// </summary> | |
public void ClearAllPreferences () { | |
PlayerPrefs.DeleteKey (BgMusicVolKey); | |
PlayerPrefs.DeleteKey (SoundFxVolKey); | |
PlayerPrefs.DeleteKey (BgMusicMuteKey); | |
PlayerPrefs.DeleteKey (SoundFxMuteKey); | |
PlayerPrefs.Save (); | |
} | |
/// <summary> | |
/// 모든 사운드 옵션을 PlayerPrefs에 저장하는 함수 | |
/// </summary> | |
public void SaveAllPreferences () { | |
PlayerPrefs.SetFloat (SoundFxVolKey, _soundFxVolume); | |
PlayerPrefs.SetFloat (BgMusicVolKey, _musicVolume); | |
PlayerPrefs.SetInt (SoundFxMuteKey, _soundFxOn ? 1 : 0); | |
PlayerPrefs.SetInt (BgMusicMuteKey, _musicOn ? 1 : 0); | |
PlayerPrefs.Save (); | |
} | |
/// <summary> | |
/// 오디오 클립 리스트를 초기화하는 함수 | |
/// </summary> | |
public void EmptyPlaylist () { | |
_playlist.Clear (); | |
} | |
/// <summary> | |
/// 오디오 클립 리스트에 오디오 클립을 추가하는 함수 | |
/// </summary> | |
/// <param name="clip">오디오 클립</param> | |
public void AddToPlaylist (AudioClip clip) { | |
if (clip != null) { | |
_playlist.Add (clip); | |
} | |
} | |
/// <summary> | |
/// 오디오 클립 리스트에 오디오 클립을 제거하는 함수 | |
/// </summary> | |
/// <param name="clip">오디오 클립</param> | |
public void RemoveFromPlaylist (AudioClip clip) { | |
if (clip != null && GetClipFromPlaylist (clip.name)) { | |
_playlist.Remove (clip); | |
_playlist.Sort ((x, y) => x.name.CompareTo (y.name)); | |
} | |
} | |
/// <summary> | |
/// 오디오 이름으로 오디오 클립 리스트에서 오디오 클립 가져오는 함수 | |
/// </summary> | |
/// <param name="clip_name">클립 이름</param> | |
/// <returns>The AudioClip from the pool or null if no matching name can be found</returns> | |
public AudioClip GetClipFromPlaylist (string clip_name) { | |
for (int i = 0; i < _playlist.Count; i++) { | |
if (clip_name == _playlist[i].name) { | |
return _playlist[i]; | |
} | |
} | |
Debug.LogWarning (clip_name + " does not exist in the playlist."); | |
return null; | |
} | |
/// <summary> | |
/// Resources 폴더 경로에 있는 모든 오디오 클립을 오디오 클립 리스트에 가져오는 함수 | |
/// </summary> | |
/// <param name="path">Resoures 폴더 내 폴더경로 예) "" 입력 시 Resources 내 모든 클립을 가져옴.</param> | |
/// <param name="overwrite">덮어씌울지 여부, true - 리스트 덮어씌움, false - 리스트에 연달아서 추가</param> | |
public void LoadPlaylist (string path, bool overwrite) { | |
AudioClip[] clips = Resources.LoadAll<AudioClip> (path); | |
// 새로운 리스트로 덮어씌울지 체크 | |
if (clips != null && clips.Length > 0 && overwrite) { | |
_playlist.Clear (); | |
} | |
for (int i = 0; i < clips.Length; i++) { | |
_playlist.Add (clips[i]); | |
} | |
} | |
/// <summary> | |
/// 현재 배경음 클립을 가져오는 속성 | |
/// </summary> | |
/// <value>The current music clip.</value> | |
public AudioClip CurrentMusicClip { | |
get { return backgroundMusic.CurrentClip; } | |
} | |
/// <summary> | |
/// 효과음 풀을 가져오는 속성 | |
/// </summary> | |
public List<SoundEffect> SoundFxPool { | |
get { return sfxPool; } | |
} | |
/// <summary> | |
/// 오디오 매니저의 클립 리스트를 가져오는 속성 | |
/// </summary> | |
public List<AudioClip> Playlist { | |
get { return _playlist; } | |
} | |
/// <summary> | |
/// 배경음이 재생중인지 체크하는 속성 | |
/// </summary> | |
public bool IsMusicPlaying { | |
get { return musicSource != null && musicSource.isPlaying; } | |
} | |
/// <summary> | |
/// 배경음 사운드 크기를 가져오거나 지정하는 속성 | |
/// </summary> | |
/// <value>사운드 크기</value> | |
public float MusicVolume { | |
get { return _musicVolume; } | |
set { SetBGMVolume (value); } | |
} | |
/// <summary> | |
/// 효과음 사운드 크기를 가져오거나 지정하는 속성 | |
/// </summary> | |
/// <value>사운드 크기</value> | |
public float SoundVolume { | |
get { return _soundFxVolume; } | |
set { SetSFXVolume (value); } | |
} | |
/// <summary> | |
/// 배경음 On/Off 체크하거나 지정하는 속성 | |
/// </summary> | |
/// <value><c>true</c> - BGM On; <c>false</c> - BGM Off</value> | |
public bool IsMusicOn { | |
get { return _musicOn; } | |
set { ToggleBGMMute (value); } | |
} | |
/// <summary> | |
/// 효과음 On/Off 체크하거나 지정하는 속성 | |
/// </summary> | |
/// <value><c>true</c> - SFX On; <c>false</c> - SFX Off</value> | |
public bool IsSoundOn { | |
get { return _soundFxOn; } | |
set { ToggleSFXMute (value); } | |
} | |
/// <summary> | |
/// 배경음과 효과음 On/Off 체크하거나 지정하는 속성 | |
/// </summary> | |
/// <value><c>true</c> - BGM+SFX On; <c>false</c> - BGM+SFX Off</value> | |
public bool IsMasterMute { | |
get { return !_musicOn && !_soundFxOn; } | |
set { ToggleMute (value); } | |
} | |
} | |
/// <summary> | |
/// 전환효과 | |
/// </summary> | |
public enum MusicTransition { | |
/// <summary> | |
/// (없음) 다음음악이 즉시 재생 | |
/// </summary> | |
Swift, | |
/// <summary> | |
/// (페이드 인/아웃) 페이드 아웃되고 다음 음악 페이드 인 | |
/// </summary> | |
LinearFade, | |
/// <summary> | |
/// (크로스) 현재음악과 다음음악이 크로스 | |
/// </summary> | |
CrossFade | |
} | |
/// <summary> | |
/// 배경음 설정 | |
/// </summary> | |
[System.Serializable] | |
public struct BackgroundMusic { | |
/// <summary> | |
/// 배경음 현재 클립 | |
/// </summary> | |
public AudioClip CurrentClip; | |
/// <summary> | |
/// 배경음 다음 클립 | |
/// </summary> | |
public AudioClip NextClip; | |
/// <summary> | |
/// 전환효과 | |
/// </summary> | |
public MusicTransition MusicTransition; | |
/// <summary> | |
/// 전환효과 시간 | |
/// </summary> | |
public float TransitionDuration; | |
} | |
/// <summary> | |
/// 효과음 구조와 설정 | |
/// </summary> | |
[System.Serializable] | |
public class SoundEffect : MonoBehaviour { | |
[SerializeField] private AudioSource audioSource; | |
[SerializeField] private float originalVolume; | |
[SerializeField] private float duration; | |
[SerializeField] private float playbackPosition; | |
[SerializeField] private float time; | |
[SerializeField] private Action callback; | |
[SerializeField] private bool singleton; | |
/// <summary> | |
/// 효과음 이름 속성 | |
/// </summary> | |
/// <value>이름</value> | |
public string Name { | |
get { return audioSource.clip.name; } | |
} | |
/// <summary> | |
/// 효과음 길이 속성 (초 단위) | |
/// </summary> | |
/// <value>길이</value> | |
public float Length { | |
get { return audioSource.clip.length; } | |
} | |
/// <summary> | |
/// 효과음 재생된 시간 속성 (초 단위) | |
/// </summary> | |
/// <value>재생된 시간</value> | |
public float PlaybackPosition { | |
get { return audioSource.time; } | |
} | |
/// <summary> | |
/// 효과음 클립 속성 | |
/// </summary> | |
/// <value>오디오 클립</value> | |
public AudioSource Source { | |
get { return audioSource; } | |
set { audioSource = value; } | |
} | |
/// <summary> | |
/// 효과음 원본 볼륨 속성 | |
/// </summary> | |
/// <value>원본 사운드 크기</value> | |
public float OriginalVolume { | |
get { return originalVolume; } | |
set { originalVolume = value; } | |
} | |
/// <summary> | |
/// 효과음 총 재생시간 속성 (초단위) | |
/// </summary> | |
/// <value>총 재생시간</value> | |
public float Duration { | |
get { return duration; } | |
set { duration = value; } | |
} | |
/// <summary> | |
/// 효과음 남은 재생시간 속성 (초단위) | |
/// </summary> | |
/// <value>남은 재생시간</value> | |
public float Time { | |
get { return time; } | |
set { time = value; } | |
} | |
/// <summary> | |
/// 효과음 정규화된 재생진행도 속성 (정규화 0~1) | |
/// </summary> | |
/// <value>정규화된 재생진행도</value> | |
public float NormalisedTime { | |
get { return Time / Duration; } | |
} | |
/// <summary> | |
/// 효과음 완료 시 콜백 액션 속성 | |
/// </summary> | |
/// <value>콜백 액션</value> | |
public Action Callback { | |
get { return callback; } | |
set { callback = value; } | |
} | |
/// <summary> | |
/// 효과음 반복 시 싱글톤 여부, 반복할 경우에 true 아니면 false | |
/// </summary> | |
/// <value><c>true</c> 반복 시; 그 외, <c>false</c>.</value> | |
public bool Singleton { | |
get { return singleton; } | |
set { singleton = value; } | |
} | |
} | |
------- Scroll View 생성 시 -------------- | |
자동으로 Contents View 사이즈 늘리고 싶으면 Content오브젝트에 Contents Size Fitter 컴포넌트 추가하면 된다. | |
Min Size로 설정 | |
-------- UI Texture Code 생성할 때 ---------- | |
마지막에 Apply 해줘야 됨!! 안하면 제대로 생성 안된다. | |
dummyTex = new Texture2D(1, 1); | |
dummyTex.SetPixel(0, 0, Color.black); | |
dummyTex.Apply(); | |
fadeTexture = dummyTex; | |
backgroundStyle.normal.background = fadeTexture; | |
-------- Dropbox 옵션 검색 --------------- | |
c.transform.Find("OBJInput").GetComponent<Dropdown>().value = c.transform.Find("OBJInput").GetComponent<Dropdown>().options.FindIndex( option => option.text.IndexOf(lineSep[1]) >= 0 ); | |
람다식으로 string 찾아서 적용가능하다. | |
-------- UI Invert Mask 마스크 반전 하는 코드 ---------- | |
메터리얼 두개 만든다음 Shader -> UI/Default한다음 아래와 같이 수정한다음 마스크, 마스크 될 이미지에 메터리얼 씌우면 된다. | |
※주의 이 때 Mask 컴포넌트 필요없음 그냥 Image에 적용하면 됨! | |
---- Mask Material | |
Tint Color (255,255,255,1) //using alpha of 1 gives crispest edge | |
Stencil Comparison 8 | |
Stencil ID 1 | |
Stencil Operation 2 | |
Stencil Write Mask 255 | |
Stencil Read Mask 255 | |
Color Mask 0 // use 15 if you want to see the mask graphic (0 vs RGB 1110) | |
Use Alpha Clip True // toggles if the graphic affects the mask, or just the geometry | |
---- Masked Material | |
Tint Color (255,255,255,255) // not important | |
Stencil Comparison 3 | |
Stencil ID 2 // default Unity mask has 1 here. this is the swap. I think it's GEqual => Less | |
Stencil Operation 0 | |
Stencil Write Mask 0 | |
Stencil Read Mask 1 | |
Color Mask 15 | |
Use Alpha Clip False | |
---------- 카메라 이동중일때 스크롤로 속도 조정 가능 --------------- | |
x0.1 - x2.0 | |
---------- 애니메이션 특정 시간부터 실행되게 하는 코드 --------- | |
Animation이면 .time = 원하는 시간; 후 Play()로 실행하면 되고 | |
Animator이면 .Play("원하는 스테이트 명", 0(Layer), 0-1.0(퍼센트로)); 실행시키면 된다. | |
-------- 인스펙터 뷰 멀티 와 Debug모드 ------- | |
인스펙터 뷰에서 오른쪽 위에 창 메뉴 클릭해서 인스펙터 뷰 띄우고 잠금하면 여러개 띄울 수 있고 | |
Debug모드로 변경하면 컴포넌트의 숨겨진(보이지 않던) 옵션을 수정/ 볼 수 있다. | |
-------- 델리게이트, 이벤트, 관찰자 패턴 -------- | |
event의경우 delegate를 담을 그릇으로 보시면되고 | |
delegate의경우 method를 담는그릇으로 보시면 됩니다. | |
만약에 게임전체에서 사용될 POPUP(YES , NO버튼을 가진)에서 사용될 popup class를 구현할시 | |
public class Popup : Pannel{ | |
public delegate void PopUpEvent(); // void 타입 delegate 선억 | |
public event PopUpEvent ClickYes; // yes클릭시 실행할 event선언 | |
public event PopUpEvent ClickNo; // no클릭시 실행할 event선언 | |
public void ClickYes(){ ClickYes(); } | |
public void ClickNo(){ ClickNo(); } | |
} | |
이런식으로 구현을 해두면 추후에 popup을 활용히 void()타입의 method는 뭐든지 (yes/no버튼의) | |
콜백 이벤트로 사용할수 있습니다. | |
public void testEvent(){ | |
// yes 버튼 눌렸을때 실행 | |
} | |
이런식의 void 형 method를 콜백으로 지정해주고싶으시면 | |
ClickYes += testEvent; 이런식으로 담을수있습니다. | |
ClickYes -= testEvent; 이런식으로 뺄수도 있구요 ^^ | |
event와 delegate를 | |
property와 멤버 변수의 관계랑 비슷하다고 생각하시면 됩니다. | |
event는 property가 get, set을 갖는 것 처럼 add, remove함수를 가져야 합니다. | |
근데 사용하기 쉽게 add, remove를 생략하실수 있습니다. | |
public event Handler Event; // 생략형 | |
그냥 이렇게 하면 아래와 같은 코드가 만들어집니다. | |
private Handler _Event; // _Event라는 delegate 변수를 자동으로 만들어냅니다. | |
public event Handler Event | |
{ | |
add | |
{ | |
lock(this) { _Event += value; } | |
} | |
remove | |
{ | |
lock(this) { _Event -= value; } | |
} | |
} | |
이 Event 이벤트 변수에다가 +=을 써주면 add가 불리고 -=을 써주면 remove가 불리는 겁니다. | |
그래서 겉에서 봤을땐 delegate랑 사용법이 똑같이 보이게 됩니다. | |
근데 이것도 약간 다른 점이 이벤트 호출시에 Event("event");를 사용하는데 | |
이건 클래스 안에서만 사용 가능합니다. 클래스 밖에서 test.Event("event"); 하면 에러가 나지요 | |
생략형으로 쓰면 c#이 자동으로 Event() 호출을 _Event 대리자의 호출로 치환해 줍니다. | |
_Event는 내부 변수이므로 밖에서 호출이 불가능합니다. | |
add, remove를 명시적으로 쓸 경우에는 Event("event"); 이런식으로 호출을 못합니다(자동으로 대리자 변수를 만들어내지 않기 때문에) | |
직접 _Event("event"); 이런식으로 대리자를 호출해 주거나 그에 상응하는 작업을 구현해야 합니다. | |
이러한 차이점이 있기 때문에 event는 interface에서 선언해 줄수 있고요.. delegate는 그렇지 못합니다. | |
event변수가 넘쳐나는걸 막기 위해 winforms에서는 메시지 이벤트를 add, remove를 직접 구현하고 dictionary에 event를 저장하는 방식을 쓴다고 합니다. | |
http://msdn2.micros...ibrary/z4ka55h8.aspx | |
여기 Events 부분에 자세한 설명이 있습니다. | |
http://www.yoda.ara...m/csharp/events.html | |
예를들어 팝업창을 띄우는 함수 | |
void 팝업오픈(string titlestr, string instr) | |
{ | |
팝업창 UI 오픈 | |
팝업창 제목 = titlestr; | |
팝업창 내용 = instr; | |
OK 버튼 누르면 = 재화상점으로 이동; | |
} | |
이런 내용의 함수가 있다고 합시다. | |
여러곳에서 팝업을 띄워야 하는데.. 팝업 띄울 때마다 저 함수를 각각의 스크립트에 만들어서 호출하면 굉장히 비효율적이겠죠? | |
왜 비효율적이냐 하면 예를들어서 "재화가 모자랍니다" 라는 내용의 팝업을 띄운다고 합시다 | |
OK를 누르면 어떤 경우에는 금화 상점으로 이동시키고, 어떤 경우에는 캐시 상점으로 이동시켜야 합니다 | |
이럴 때 델리게이트를 쓰지않으면 모든 스크립트마다 저 팝업 함수를 만들어 놓고 OK 버튼을 누르면 각각의 스크립트에서 어떻게 행동해야 할지를 지정해 주어야 합니다 | |
만약에 하나의 스크립트에서 다양한 팝업이 떠야 하는 분기가 있다면 모든 종류의 팝업에 대해서 죄다 하드코딩으로 일일이 만들어줘야 하는 불상사가 생기겠죠 ㅋㅋ | |
그래서 전역클래스 같은데다가 팝업오픈 함수를 만들어놓고 OK를 눌렀을때 어떤 함수를 실행시킬지를 전달(델리게이트)해 주는 겁니다 | |
이런방식으로요.. | |
void 팝업오픈(string titlestr, string instr, callbackDelegate func) | |
{ | |
팝업창 UI 오픈 | |
팝업창 제목 = titlestr; | |
팝업창 내용 = instr; | |
OK 버튼 누르면 = 인자로 받은 func 함수 호출 | |
} | |
-내 생각 - | |
델리게이트 걍 | |
ButtonComp(이벤트쓸컴포넌트).OnClick(이벤트).AddListener(delegate { 동작할 함수; }); | |
이런식으로 간편 코딩할 때 가장 유용한듯 ㅎㅎ | |
도전과제랑 | |
------- 충돌처리 실무편 (충돌체에 리지드바디 달아두고, 피충돌체에는 콜리더 둔다음 이런코드) -------- | |
void OnCollisionStay(Collision other) | |
{ | |
if (other.transform) //Null 여부 판단하기위해 | |
{ | |
ContactPoint contact = other.contacts[0]; //매우 신비로운 ContactPoint | |
Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal) * Quaternion.Euler(-90, 0, 0); //Normalize해버리는 | |
Vector3 pos = contact.point; //이렇게 기본적으로 쓰구 | |
if (sparks) | |
{ | |
GameObject spark = (GameObject)Instantiate(sparks, pos, rot); | |
spark.transform.localScale = transform.localScale * 2; | |
foreach (Transform _spark in spark.transform) // 이런식으로 foreach로 Transform으로 자식들 쉽게 적용하는 법 인용하자 | |
{ | |
_spark.localScale = transform.localScale * 2; | |
} | |
StartCoroutine(SparksCleaner(spark)); //이런식으로 간단한파티클이면 생성한다음 제거하는 코루틴 1초뒤에 뭐 이런식으로 제작 | |
} | |
else | |
{ | |
Debug.LogError("You did not assign a spark prefab effect, default is located in DroneController/Prefabs/..."); | |
} | |
} | |
} | |
------- 2D Trajectory Prediction (2D 슈팅 가이드 라인) -------- | |
https://www.windykeep.com/2017/07/07/trajectory-prediction-angry-birds-style-with-applications-in-unity/ | |
------- C#에서 단축 키워드 만들기 == C++ 약간 알고리즘에서 자료구조 단순화하는것 처럼 ---------- | |
using Pair = System.Collections.Generic.KeyValuePair<UnityEngine.Vector2, UnityEngine.Vector2>; | |
이런식으로 위에 선언하면 | |
private Pair[] ~~ 이런식으로 쓸 수 있다. | |
원래는 private KeyValuePair<Vector2, Vector2>[] ~~ 이렇게 써야하는데 ㅎㅎ | |
------- UI드래그 하거나 뭐 그럴 때 ------ | |
using UnityEngine.EventSystems; | |
하고나서 인터페이스 IBeginDragHandler, IEndDragHandler, IDragHandler 이거 필요한거 가져다가 | |
OnDrag()나 OnBeginDrag() OnEndDrag() 추상메서드 구현해주면 된다. | |
넘 편해 | |
-------- Tuple(인자3개이상 자료형) 간단하게 사용하는 방법 --------- | |
List<(int, string)> list = new List<(int, string)>(); | |
list.Add((3, "test")); | |
list.Add((6, "second")); | |
이런식으로 넣을수있다. | |
List<(int MyInteger, string MyString)> result = Method(); | |
var firstTuple = result.First(); | |
int i = firstTuple.MyInteger; | |
string s = firstTuple.MyString; | |
이런식으로 받을수도 있고Item1, Item2, Itme3.. 이렇게 받을수도 있음. | |
------- Find 비활성화 찾기 문제와 비용 ------- | |
GameObject.Find 는 비활성화된 오브젝트를 감지하지 못하지만 (전체에서 찾는것 비용이 더 비쌀 수 밖에) | |
transform.Find 는 비활성화된 오브젝트도 감지한다. (특정 오브젝트 기준 하위에서 찾는 것 저렴할 수 밖에) | |
그리구 비활성화되도 GetChildCount만큼 포문돌려서 transfrom 접근해서 가져오면 비활성화도 다 가져 올 수 있다. | |
------- 2D 레이어 개념 ----------- | |
우선 순위 높은 것 | |
1. z가 카메라와 가까울 수록 앞 (사실 상 강제 레이어 개념이 적용 안될 때 사용) | |
2. Sort Layer가 높을수록 = 아래있을 수록 앞 | |
3. Sort Layer가 같은 녀석은 Order가 높을수록 앞 | |
4. 다 같을 땐 하이라키에서 자식일수록> 같은 시블링이면 아래있을수록 앞 | |
텍스트 메쉬 Text Mesh는, 텍스트 문자열을 표시하는 3D 지오메트리 이므로 2D일 경우 사용 니니 Text GUI를 사용하라고 나온다. | |
걍 Text 사용하면 댐 2D | |
레이어 적용 안되는 애 강제로 넣는 방법 | |
using UnityEngine; | |
using System.Collections; | |
public class Push3DtoFront : MonoBehaviour | |
{ | |
public string layerToPushTo; | |
void Start () | |
{ | |
GetComponent<Renderer>().sortingLayerName = layerToPushTo; | |
//Debug.Log(GetComponent<Renderer>().sortingLayerName); | |
} | |
} | |
-------- Shadow Distance --------- | |
그림자 보이는 범위 지정하는 것 Edit > Project Settings > Quality > Shadow Settings 에서 지정할 수 있다. | |
Window > Rendering > Lighting 에서 Auto Generate 누르면 자동으로 그림자를 볼 수 있다. | |
-------- Terrain 에서 F 눌러서 포커싱할 수 있다. ------------- | |
-------- (특정 시간 내에) 미끄러지듯이 움직이기 Mathf.SmoothDamp() -------------- | |
Vector3.SmoothDamp도 있다. | |
static function SmoothDamp (current:float, target:float, ref currentVelocity:float, smoothTime:float, maxSpeed:float = Mathf.Infinity, deltaTime:float = Time.deltaTime) : float | |
current : 현재 위치 | |
target : 타겟 위치 | |
currentVelocity : 호출할때마다 이 함수에 의해 변경( 계산 )되는 현재속도 | |
smoothTime : 현재 위치에서 목적 위치까지 이르는데 걸리는 시간. 이 값이 작을수록 목적지에 빠르게 도착한다. | |
maxSpeed : 스피드의 상한치. | |
deltaTime : By default Time.deltaTime. | |
시간 내에 정해진 목표(값)로 점점 값을 변화시킨다. | |
결코 특정값을 넘지 않게 하는 스프링 제동 기능으로써 값을 자연스럽게 변화하게 한다. | |
이 함수는 어떤 종류의 값, 위치, 색, 스칼라등의 어떤 종류의 값이라도 자연스럽게 변화시키는데 이용될수 있다. | |
ex) | |
using UnityEngine; | |
using System.Collections; | |
public class example : MonoBehaviour { | |
public Transform _target; | |
public float _smoothTime = 0.3F; | |
private float _yVelocity = 0.0F; | |
void Update() { | |
float newPosition = Mathf.SmoothDamp(transform.position.y, target.position.y, ref yVelocity, smoothTime); | |
transform.position = new Vector3(transform.position.x, newPosition, transform.position.z); | |
} | |
} | |
-------- WebGL에서 파일 다운/업로드하고 싶을때or 웹 연동 코드 짜고싶을 때 ---- | |
.jslib 파일을 Assets/Plugins/ 폴더안에 만들어놓고 | |
아래와 같은 형식으로 사용하면 된다. | |
//텍스트 파일 다운로드 예제 이걸로 하면 브라우저 다 지원가능 (다운로드 폴더로 받아짐) | |
var TextDownloaderPlugin = { | |
TextDownloader: function(str, fn) { | |
var msg = Pointer_stringify(str); | |
var fname = Pointer_stringify(fn); | |
var data = new Blob([msg], {type: 'text/plain'}); | |
var link = document.createElement('a'); | |
link.download = fname; | |
link.innerHTML = 'DownloadFile'; | |
link.setAttribute('id', 'TextDownloaderLink'); | |
if(window.webkitURL != null) | |
{ | |
link.href = window.webkitURL.createObjectURL(data); | |
} | |
else | |
{ | |
link.href = window.URL.createObjectURL(data); | |
link.onclick = function() | |
{ | |
var child = document.getElementById('TextDownloaderLink'); | |
child.parentNode.removeChild(child); | |
}; | |
link.style.display = 'none'; | |
document.body.appendChild(link); | |
} | |
link.click(); | |
} | |
}; | |
mergeInto(LibraryManager.library, TextDownloaderPlugin); | |
-------- 키보드 입력 한게 뭔지 궁금할 때 ------------- | |
Input.inputString -> 키보드 문자만 나오고 스페이스나 이런 엔터는 안나옴 | |
using UnityEngine; | |
using System.Collections; | |
public class example : MonoBehaviour | |
{ | |
void OnGUI() | |
{ | |
Event e = Event.current; | |
if (e.isKey) | |
{ | |
Debug.Log("Detected a keyboard event!" + e.keyCode ); | |
} | |
} | |
} | |
event = Event.current; | |
if(event.isKey){ | |
if(event.type == EventType.KeyDown){ | |
Debug.Log(event.keyCode); | |
} | |
} | |
------- 텍스트 컴포넌트에서 글자별로 크기 다르게 하고싶을때 ----- | |
rich text 쓰시면 됩니다 | |
<size = 10> a </size> <size = 11> b </size> <size = 12> c </size> <size = 13> d </size> | |
------- 미니맵 만들기 -------------------- | |
Create > Renderer Texture 해서 Texture 만든다음 | |
카메라 하나 두고 Target Texture에 만든 텍스쳐 넣고 | |
RawImage로 집어넣으면 됨. | |
참고 : https://www.youtube.com/watch?v=28JTTXqMvOU | |
-------- 코루틴 실행중인에 멈출려면 변수에 할당해야한다. ------ | |
IEnumerator 변수; | |
변수 = (IEnumerator) 코루틴할당; | |
StartCoroutine(변수); | |
StorCoroutine(변수); | |
IEnumerator 함수 정의; | |
-------- 데이터 저장할 때 JSON으로 -------- | |
난 Resources에 게임 최초데이터 놓고, 기존 Application.persistentDataPath에 저장한 데이터가 없으면 저걸로 불러오면서 저장하고 | |
아님 persistentPath에 있는 데이터 불러오거나 저장하는 식으로 진행했다. | |
Application.persistentDataPath 이 폴더는 안드로이드 , 아이폰 내부에 그 app이 쓸수 있는 허락받은 공간의 제일 윗 폴더를 가르키고 있습니다. | |
+ 폴더 생성하는 법 | |
Debug.LogWarning("There is no file on local."); | |
// Create New Folder | |
string sDirPath; | |
sDirPath = Application.persistentDataPath + "/root/configuration"; | |
DirectoryInfo di = new DirectoryInfo(sDirPath); | |
if (di.Exists == false) | |
{ | |
di.Create(); | |
} | |
// Write File | |
FileStream fs = new FileStream(Application.persistentDataPath + "/root/configuration/configuration.json", FileMode.CreateNew); | |
byte[] info = new UTF8Encoding(true).GetBytes(source); | |
fs.Write(info, 0, info.Length); | |
-------- 씬 페이드 아웃 -------- | |
씬 바꾸는 스크립트를 만들어두고, 재사용하는 식으로 사용하는 것 좋은방법이다. | |
Ex) public으로 선언해서 에디터에서 수정하는 식으로 | |
public class SceneTransition : MonoBehaviour | |
{ | |
public string scene = "<Insert scene name>"; | |
public float duration = 1.0f; | |
public Color color = Color.black; | |
public void PerformTransition() | |
{ | |
Transition.LoadLevel(scene, duration, color); | |
} | |
} | |
아니면 카메라에 달아두는 것도 스크립트 난 이 방법 씀. | |
using UnityEngine; | |
public class CameraFade : MonoBehaviour | |
{ | |
// ---------------------------------------- | |
// PUBLIC FIELDS | |
// ---------------------------------------- | |
// Alpha start value | |
public float startAlpha = 1; | |
// Texture used for fading | |
public Texture2D fadeTexture; | |
// Default time a fade takes in seconds | |
public float fadeDuration = 2; | |
// Depth of the gui element | |
public int guiDepth = -5; | |
// Fade into scene at start | |
public bool fadeIntoScene = true; | |
// ---------------------------------------- | |
// PRIVATE FIELDS | |
// ---------------------------------------- | |
// Current alpha of the texture | |
private float currentAlpha = 1; | |
// Current duration of the fade | |
private float currentDuration; | |
// Direction of the fade | |
private int fadeDirection = -1; | |
// Fade alpha to | |
private float targetAlpha = 0; | |
// Alpha difference | |
private float alphaDifference = 0; | |
// Style for background tiling | |
private GUIStyle backgroundStyle = new GUIStyle(); | |
private Texture2D dummyTex; | |
// Color object for alpha setting | |
Color alphaColor = new Color(); | |
// ---------------------------------------- | |
// FADE METHODS | |
// ---------------------------------------- | |
public void FadeIn(float duration, float to) | |
{ | |
// Set fade duration | |
currentDuration = duration; | |
// Set target alpha | |
targetAlpha = to; | |
// Difference | |
alphaDifference = Mathf.Clamp01(currentAlpha - targetAlpha); | |
// Set direction to Fade in | |
fadeDirection = -1; | |
} | |
public void FadeIn() | |
{ | |
FadeIn(fadeDuration, 0); | |
} | |
public void FadeIn(float duration) | |
{ | |
FadeIn(duration, 0); | |
} | |
public void FadeOut(float duration, float to) | |
{ | |
// Set fade duration | |
currentDuration = duration; | |
// Set target alpha | |
targetAlpha = to; | |
// Difference | |
alphaDifference = Mathf.Clamp01(targetAlpha - currentAlpha); | |
// Set direction to fade out | |
fadeDirection = 1; | |
} | |
public void FadeOut() | |
{ | |
FadeOut(fadeDuration, 1); | |
} | |
public void FadeOut(float duration) | |
{ | |
FadeOut(duration, 1); | |
} | |
// ---------------------------------------- | |
// STATIC FADING FOR MAIN CAMERA | |
// ---------------------------------------- | |
public static void FadeInMain(float duration, float to) | |
{ | |
GetInstance().FadeIn(duration, to); | |
} | |
public static void FadeInMain() | |
{ | |
GetInstance().FadeIn(); | |
} | |
public static void FadeInMain(float duration) | |
{ | |
GetInstance().FadeIn(duration); | |
} | |
public static void FadeOutMain(float duration, float to) | |
{ | |
GetInstance().FadeOut(duration, to); | |
} | |
public static void FadeOutMain() | |
{ | |
GetInstance().FadeOut(); | |
} | |
public static void FadeOutMain(float duration) | |
{ | |
GetInstance().FadeOut(duration); | |
} | |
// Get script fom Camera | |
public static CameraFade GetInstance() | |
{ | |
// Get Script | |
CameraFade fader = (CameraFade) Camera.main.GetComponent("CameraFade"); | |
// Check if script exists | |
if (fader == null) | |
{ | |
Debug.LogError("No FadeInOut attached to the main camera."); | |
} | |
return fader; | |
} | |
// ---------------------------------------- | |
// SCENE FADEIN | |
// ---------------------------------------- | |
public void Start() | |
{ | |
Debug.Log("Starting FadeInOut"); | |
dummyTex = new Texture2D(1, 1); | |
dummyTex.SetPixel(0, 0, Color.black); | |
fadeTexture = dummyTex; | |
backgroundStyle.normal.background = fadeTexture; | |
currentAlpha = startAlpha; | |
if (fadeIntoScene) | |
{ | |
FadeIn(); | |
} | |
} | |
// ---------------------------------------- | |
// FADING METHOD | |
// ---------------------------------------- | |
public void OnGUI() | |
{ | |
// Fade alpha if active | |
if ((fadeDirection == -1 && currentAlpha > targetAlpha) || | |
(fadeDirection == 1 && currentAlpha < targetAlpha)) | |
{ | |
// Advance fade by fraction of full fade time | |
currentAlpha += (fadeDirection * alphaDifference) * (Time.deltaTime / currentDuration); | |
// Clamp to 0-1 | |
currentAlpha = Mathf.Clamp01(currentAlpha); | |
} | |
// Draw only if not transculent | |
if (currentAlpha > 0) | |
{ | |
// Draw texture at depth | |
alphaColor.a = currentAlpha; | |
GUI.color = alphaColor; | |
GUI.depth = guiDepth; | |
GUI.Label(new Rect(-10, -10, Screen.width + 10, Screen.height + 10), dummyTex, backgroundStyle); | |
} | |
} | |
} | |
---------- Json 파일 이쁘게 정렬하는 법 -------------- | |
There are two overloads for the JsonUtility.ToJson function: | |
public static string ToJson(object obj); | |
public static string ToJson(object obj, bool prettyPrint); | |
Use the second one and pass true to it. It will format the output for readability making the json separated into lines. | |
Just replace string json = JsonUtility.ToJson(actors); with string json = JsonUtility.ToJson(actors, true); | |
If you are not satisfied with the result, use Newtonsoft.Json for Unity and format the json like this: | |
string json = JsonConvert.SerializeObject(actors); | |
string newLineJson = JValue.Parse(json).ToString(Formatting.Indented); | |
--------- 파티클 Camera Overlay UI에 적용하는법 --------- | |
UI에 파티클 넣으려하면 Camera Overlay일 경우 Renderer Sorting Layer 바꿔도 소용없는데 이 때 | |
UnityUIExtensions 플러그인을 추가한 후 | |
UI에 파티클 생성한다음 | |
UIParticleSystem.cs 스크립트를 적용하고 | |
Material를 하나 만들어서 원하는 모양 UIParticle/Additave로 쉐이더 선택 후 적용해서 파티클을 UI앞에 둘 수 있다. | |
UnityUIExtensions 다운 사이트 - https://bitbucket.org/UnityUIExtensions/unity-ui-extensions/wiki/Home | |
--------- 삼각함수 활용 ---------- | |
만약 30도의 각이 있고 x축의 길이에 따라 y값을 증가시키고 싶을때 y+(x2-x1)*Mathf.Tan(30.0f*Mathf.Deg2Rad) 이런식으로 응용하면 됨. | |
-------- JSON Pasing ------ | |
JsonUtillity 5.3이후에 추가되서 이거 쓰면된다. | |
JsonUtility의 가장 큰 단점이 배열을 받을 수 없다는 것인데 | |
다음과 같이 JsonObject를 감싸는(JsonObject 배열을 가지는) Wrapper 클래스를 따로 만들어서 문제를 해결할 수 있다. | |
using System; | |
using UnityEngine; | |
public static class JsonHelper | |
{ | |
public static T[] FromJson<T>(string jsonArray) | |
{ | |
jsonArray = WrapArray (jsonArray); | |
return FromJsonWrapped<T> (jsonArray); | |
} | |
public static T[] FromJsonWrapped<T> (string jsonObject) | |
{ | |
Wrapper<T> wrapper = JsonUtility.FromJson<Wrapper<T>>(jsonObject); | |
return wrapper.items; | |
} | |
private static string WrapArray (string jsonArray) | |
{ | |
return "{ \"items\": " + jsonArray + "}"; | |
} | |
public static string ToJson<T>(T[] array) | |
{ | |
Wrapper<T> wrapper = new Wrapper<T>(); | |
wrapper.items = array; | |
return JsonUtility.ToJson(wrapper); | |
} | |
public static string ToJson<T>(T[] array, bool prettyPrint) | |
{ | |
Wrapper<T> wrapper = new Wrapper<T>(); | |
wrapper.items = array; | |
return JsonUtility.ToJson(wrapper, prettyPrint); | |
} | |
[Serializable] | |
private class Wrapper<T> | |
{ | |
public T[] items; | |
} | |
} | |
+ 패치됬는지 모르는데 리스트 형태로 받아올 수 있더라. | |
---------싱글톤 -------- | |
C# 6.0 | |
public class DataManager : MonoBehaviour | |
{ | |
public static DataManager inst=null; | |
private void Awake() { | |
inst = inst ?? this; | |
if(inst != this) Destroy(gameObject); | |
DontDestroyOnLoad(gameObject); | |
} | |
} | |
---------------------------------- | |
using UnityEngine; | |
using System.Collections; | |
public class ManagerClass : MonoBehaviour | |
{ | |
private static ManagerClass _instance = null; | |
public static ManagerClass Instance | |
{ | |
get | |
{ | |
if (_instance == null) | |
{ | |
_instance = FindObjectOfType(typeof(ManagerClass)) as ManagerClass; | |
if (_instance == null) | |
{ | |
Debug.LogError("There's no active ManagerClass object"); | |
} | |
} | |
return _instance; | |
} | |
} | |
} | |
쓸때는 | |
private void Awake() | |
{ | |
if (_instance == null) | |
{ | |
_instance = CreateGM; | |
} | |
} | |
// 다른 스크립트에서 GM 불러올 때 | |
private void Start() | |
{ | |
if (_instance == null) | |
{ | |
_instance = ManagerClass.CreateGM; | |
} | |
} | |
이런식으로 쓰면 됨 | |
---------해상도 -------- | |
16:9가 보편적이나 미래는 2:1 이 보편적으로 될 예정이다. | |
폰 해상도 가장 보편적인 해상도 1280x720 (2018년 기준) 16:9 | |
2013년도 1920:1080 16:9 | |
요즘 폰은 2560x1440 16:9 | |
최신 갤럭시는 2960x1440 18.5:9 를 채택했다. | |
구글에서 또 미래는 18:9(2:1) 비율을 원한다 | |
가장 큰 화면에 맞춰서 디자인하고 가로위주나 세로위주를 정해서 적용하면 된다. 그 부분 잘릴걸 예상하고 세로위주시 가로 잘림 | |
2220x 1080 (18.5 : 9) 1920x 1080 (16 : 9) 이렇게 디자인하는 중이다. | |
---------AssetBundle -------- | |
Resources.Load은 백그라운드 로딩이 아니기때문에 해당 리소스를 로드하고 있는 중에는 메인루틴 자체가 멈춤 | |
AssetBundle을 사용하면 비동기 로드시에도 유용함. | |
에셋번들을 왜 써야하죠? | |
유니티 게임을 빌드하게 되면 실행파일과 에셋은 고유한 포맷으로 압축, | |
보안이 되어 있어 재빌드 전에는 수정사항을 적용할 수 없습니다. | |
에셋번들은 위의 그림에서 보았듯이 에셋들을 모아 저장하고, | |
서버로부터 WWW 확장자를 이용해 다운로드할 수 있습니다. | |
그럼으로서 큰 용량에 민감한 모바일은 최초 앱 배포 시에 작은 용량을 배포하게 하고, | |
추후에 인터넷을 통해서 에셋들을 받도록 하는 방법이죠. | |
링크 : http://itmining.tistory.com/56 | |
--------- 간단하지만 매우 유용한 최적화 ---------- | |
1. 빈번하게 변경될 String이라면 거의 모든 케이스에 이런 식으로 사용하자. | |
using System.Text; | |
StringBuilder sb = new StringBuilder("내용: "); | |
sb.append("추가할내용"); | |
2. C#에서눈 const 보단 readonly를 사용하자 | |
우선 readonly의 확장성 역시 더 좋은면도 작업효율상 유용하고 static 붙이면 스태틱상수 그리고 스태틱생성자에서 첨 수정가능하다. | |
성능면만 봐서도 readonly는 상수를 가르키는 참조자로 동작하지만, const로 선언된 변수가 사용되면 변수에 대한 참조가 아닌 | |
실제 상수로 치환되어 사용 되기 때문에 다시 치환하여 코드를 생성해야 하므로, 전체 재컴파일을 해야 한다. | |
상수 처리를 항상하는 습관을 들여야한다. 함수의 인자나, 변하지 않을 값 | |
---------스플리시 화면 --------- | |
유니티 프로가 아니면 스플리시 화면 못끄고 | |
스플리시에 로고 넣는 곳에 편법으로 자기회사 로고 큰 이미지로 덮어버리는 방식으로 처리가능 | |
-------- 자연스러운 Glass (잔디) -------- | |
기본 색 옵션이 개구리니까 둘 다 흰색으로 바꾸고 세팅에서 Grass > Bending 옵션을 낮추면 더 자연스러워짐 | |
-------- 자연스러운 Tree (나무) -------- | |
WindZone 컴포넌트 추가해서 Main 0.3 정도 주면 자연스럽게 흔들거려서 좋다. | |
---------로딩 씬 --------- | |
AsyncOperation.progess 메소드 자체가 0~1사이의값을 부동소수점 형태로 return하는것이기 때문에 | |
그냥 async.progress를 찍으면 무조건 0으로 내림되서 보여진다. | |
debug(async.progress * 100f); 이런식으로 100f를 곱해주어서 올림해서 찍으면 진행률이 표시됨. | |
단, 유니티 자체가 프레임단위로 진행되기때문에 1프레임만에 다음씬을 로딩한다면 0->100 이렇게 찍힘 | |
using UnityEngine.SceneManagement; | |
[SerializeField] | |
private float minTime = 3f; //로딩씬이 유지되는 최소 시간 | |
[SerializeField] | |
private Slider sliderbar = null; //하단 슬라이더바 | |
[SerializeField] | |
private Text Tip = null; //상단 팁 텍스트 | |
[SerializeField] | |
private Transform wheel = null;//중앙 회전 이미지 | |
[SerializeField] | |
private string[] Tips = null;//팁 텍스트 모음 | |
private bool isLoad = false; //중복 실행 방지 | |
private float timer = 0; //시간 측정 | |
private Vector3 wheeleuler;//중앙 회전 이미지 오일러 각도측정용 | |
AsyncOperation async; | |
// Use this for initialization | |
private void Awake() | |
{ | |
Debug.Log(Tips.Length - 1); | |
int loc = UnityEngine.Random.Range(0, Tips.Length - 1); //배열내에서 무작위로 인덱스를 얻는다. | |
StringBuilder sb = new StringBuilder("팁 : "); | |
sb.Append(Tips[loc]); //배열 내 무작위 요소를 출력한다. | |
Tip.text = sb.ToString(); | |
} | |
AsyncOperation async; | |
void Start () { | |
StartCoroutine("LoadingScene"); | |
} | |
private void Update() | |
{ | |
wheeleuler = wheel.rotation.eulerAngles; | |
wheeleuler.z -= 3f; | |
wheel.rotation = Quaternion.Euler(wheeleuler); | |
timer += Time.deltaTime; | |
if (timer > minTime) async.allowSceneActivation = true; | |
} | |
IEnumerator LoadingScene() | |
{ | |
if(isLoad==false) | |
{ | |
isLoad = true; | |
async = SceneManager.LoadSceneAsync(SceneObjectManager.NextScene); | |
SceneObjectManager.NextScene = null; //다시 다음씬이 지정될때까지 null로 둔다. | |
async.allowSceneActivation = false; //다음 씬의 준비가 완료되더라도 바로 로딩되는걸 막는다. | |
while (async.progress<1f) | |
{ | |
sliderbar.value = async.progress; | |
yield return true; | |
} | |
} | |
} | |
---------스테이지--------- | |
1. 스테이지 형 씬 기본 구성법 | |
[맵 데이타]라는 파일을 만들어 게임씬에서 읽는 방법을 쓰고 있습니다. | |
메모장열어서 일정 규칙을 정해 입력하는 방식도 좋고 json이나 xml 등을 사용하기도 합니다. | |
간단하게 씬구조로 설명하면 | |
splash 씬 | |
(로딩씬) | |
메인 씬 | |
(로딩씬) | |
맵 or 상점 씬 | |
(로딩씬) | |
게임 씬 | |
(로딩씬)->맵 or 상점 씬 | |
으로 구성 | |
이런식으로 메모장등으로 읽기 쉽게 구성해 놓고,(당연히 실무에서는 영어로 작성하고 암호화등을 합니다.) | |
게임씬에서 해당 맵파일을 읽어와서 파싱(분석)해서 | |
거기에 맞게 오브젝트(장애물, 블럭)를 그려줍니다. | |
2. 씬 구성과 데이터 로드 상황 % | |
씬의 정보를 텍스트든 뭐든 만들어 놓고 | |
씬에 접속할 때 마다 해당 씬에 맞는 파일에서 정보를 불러들여서 동적으로 씬을 생성해야 합니다. | |
씬이 몇개가 될 지 모르는데 스테이지별로 씬을 만드는건 앱 용량도 커지고 관리도 힘들겠죠. | |
씬 생성을 위한 리소스 로드와 애셋번들은 딱히 상관이 없습니다. 그냥 방법의 차이입니다. | |
리소스 로드를 하기 위해서 애셋번들을 로드할 수는 있지만, 용량이 그렇게 크지 않다면 굳이 애셋번들을 로드하지 않고 apk에 함께 묶어도 됩니다. | |
씬 정보를 텍스트나 기타 파일로 구성한 뒤 (이하 텍스트파일이라고 함) 텍스트 파일을 읽어오면서 동적으로 씬을 생성하게 되면 | |
파일에 따라서 내용을 생성하므로 진행 %상황도 정확히 알 수 있게 됩니다. | |
3. 3가지 형태의 스테이지가 있을 경우 | |
단지 스테이지 공통으로 무언가를 추가시킬경우에는 하나의 씬으로 관리하는것에 비해, 조금 손이 더 가는면은 있습니다. | |
개발시에 유지 보수측면에서 생각해보면, 씬마다 따로 값을 입력해서 확인, 테스트 하는 번거로움보다, | |
각기 다른 씬을 저장해서 관리하는게 훨씬 용이합니다. 코드상의 번거로움도 훨씬 줄어들구요. | |
단지 위에 말씀 드렸다 싶이, 스테이지 공통 추가작업은 단순 노가다 작업이 조금 더 걸립니다. | |
하지만 유지 보수나, 테스트 작업시에 단축되는 시간이 있기에, 딱히 시간적으로도 손해보는건 아니라고 생각합니다. | |
플레이 방식마저 변한다면, 스테이지별로 관리하시는게 좋을것 같습니다. | |
4. 유니티적인 측면에서 스테이지 구성 | |
어플리케이션에서는 씬하나에 데이터로 스테이지를 구상하는 방법이 일반적이겠지요. | |
하지만 유니티는 자체가 맵툴의 기능을 가질 수 있으므로, 게임 오브젝트를 관리하는 공통적으로 사용되는 매니저와 프리팹들을 준비하고 | |
레벨디자이너가 사용할 수 있도록 에디터에 기능을 추가하는쪽이 유니티스러운 개발방법이 아닐까 생각이 듭니다. | |
특히나 작은 스테이지가 여러개 있는 구조라면 전자쪽이 더 나을것 같네요. | |
----------- 유용한 단축키 ---------- | |
F키 - 포커싱 | |
Shift + F키 - 정 가운데 위치로 | |
Ctrl Shift + F 카메라 정가운데로 오브젝트 이동 | |
Ctrl Shift + V 키 - 스냅 | |
---------- 3D 카메라 깜박일 때 ------------ | |
near distance 0.5 | |
far distacne 더 길게 10000이면 거의다 해결 됨 | |
----------게임 데이터 관리---------- | |
1. PlayerPrefs | |
장점 | |
1. 유니티에 내장되어 있음 | |
2. 직관적이고 간단한 함수 | |
3. Dictionary와 비슷 | |
단점 | |
1. 제한적(int, float, string만 가능) | |
2. 한번에 한개의 값만 불러올 수 있음 | |
3. 한개의 파일에만 저장됨 | |
* WebPlayer은 1MB의 용량 제한 있음 | |
추천 용도 | |
1. 플레이어 세팅(볼륨, 그래픽 세팅 등) | |
2. 단순한 게임의 진행도 | |
3. 스코어보드 | |
2. ScriptableObject | |
(영상 링크) | |
유니티의 오브젝트: MonoBehaviour(GameObject) + ScriptableObject | |
장점 | |
1. 유니티에 내장되어 있음 | |
2. Asset으로 저장될 수 있음 | |
3. 런타임에 저장할 수 있음 | |
4. 파싱 등을 신경쓸필요 없음 | |
5. 많은 양의 데이터 저장 가능 | |
6. 원하는 구조로 만들 수 있음 | |
단점 | |
1. 에디터 스크립팅이 필요할 수 있음 | |
2. 유니티 외부에서 수정 불가 | |
3. 빌드 후 저장 불가 | |
(플레이어 진행사항 저장이 아닌 게임 자체의 데이터 저장에 적합) | |
추천 용도 | |
1. 게임 자체 데이터 저장 | |
2. 데이터 로드 최적화 | |
3. Binary Formatter | |
오브젝트를 binary format으로 serialize/deserialize함 | |
장점 | |
1. 가장 범용적 | |
2. 가장 안전 | |
단점 | |
1. 자신이 직접 함수 만들어야 함 | |
2. 유니티 오브젝트(애니메이션, Vector3) Serialize 하기 어려움 | |
추천 용도 | |
1. 로컬 게임플레이 데이터 저장 | |
2. 변형되면 안되는 게임 콘텐츠 | |
4. XML/JSON | |
장점 | |
1. 배우기 쉬움 | |
2. 유니티에서 지원 | |
3. 전체 클래스나 단독 데이터도 저장 가능 | |
4. array, list와 잘 맞음 | |
5. 유니티 외부에서도 쉽게 수정 가능 | |
단점 | |
1. 클래스가 더러워질 수 있음 | |
2. enum 사용시 잘못 입력하면 위험할 수 있음 | |
추천용도 | |
1. 웹기반 게임 | |
2. 게임 콘텐츠 list | |
3. 인터넷 접근가능한 게임 콘텐츠 | |
4. 콘텐츠 제작 개발 툴 | |
------------ Unity WebGL(웹 HTML 빌드) 가능한 것과 불가능한 점 --------------- | |
Unity WebGL에서 할 수있는 점 | |
Unity에서 제공하는 응용 출력 할 수있는 많은 기본적인 항목을 사용할 수 있습니다. | |
그래픽 그리기 (3D / 2D) | |
· 입력의 수신 (키보드/마우스) | |
사운드의 재생, 정지 등의 기본 조작 | |
·HTTP / HTTPS통신 (WWW클래스 만) | |
자산 번들 | |
등. | |
Unity WebGL에서 할 수 없는 점 | |
전술 한 바와 같이, 많은 수있는Unity의WebGL하지만 플랫폼이 브라우저이라는 형편 상 아무래도 피할 수없는 문제가 몇 가지 있습니다. | |
또한 플러그인을 만들고HTML5사양에 따라 동작을 구현하면 회피 할 수있는 것도 있지만,JavaScript하면HTML5에 익숙해야합니다. | |
● 통신HTTP / HTTPS이외 사용하지 못한 | |
일단,FTP통신도 할 수 있습니다 만, 이쪽은 수신 만 가능합니다. | |
또한UnityEngine.WWW클래스 이외의 네트워크 기능은 기본적으로 사용할 수 없습니다. | |
● 통신 데이터를 스트림 형식으로 사용할 수없는 | |
통신은HTTP응답이 완전히 돌아 탈 때까지 데이터를 사용할 수 없습니다. | |
즉 다운로드를하면서 이미 다운로드가 끝나는 부분은 사용하는 등 수는 없습니다. | |
● 동일 호스트의 데이터에만 액세스 할 수 없다 | |
기본적으로WWW클래스에서 읽을 수있는 데이터는 동일한 호스트에서 데이터이어야합니다. | |
이것은WebGL설명서의WWW클래스의 구현XMLHttpRequest 에 의한 것으로,XMLHttpRequest이 보안 문제로 동일 출처 정책을 준수 할 것에 기인하는 것입니다. | |
이에 관해서는Unity측면에서의 대응으로 해결하지 못하고 동일한 호스트에 데이터를 배치하거나,Cross-Origin Resource Sharing (CORS)을 서버 측에 올바르게 설정하여 해결 할 수 있습니다. | |
● 스레드를 이용한 처리를 할 수 없다 | |
스레드를 사용할 수 없습니다. | |
그러나 기본적으로UnityEngine클래스에는 메인 스레드 이외의 동작은 할 수 없도록되어 있습니다. | |
아무래도 비슷한 처리를 할 필요가있는 경우 코루찐을 이용하는 등으로 해결할 수 있어야합니다. | |
● 파일 시스템이 존재하지 않기 때문에 파일이나 디렉토리를 사용할 수 없다 | |
브라우저에서 파일에 액세스 할 수있는 수단이 없기 때문입니다. | |
이들은 향후HTML5관계의 기술 사양이 추가 됨으로써 해소 될 가능성이 있습니다. | |
현상으로 대처하는 경우Indexed DB같은 가상 파일 시스템을 만드는 등하면 가상으로 파일 시스템을 구축 할 수 있습니다. | |
● 키보드에서 일본어 입력을 할 수없는 | |
uGUI입력 필드에 키보드에서 일본어 입력이 불가능합니다. | |
이것은 원래IME브라우저를 제어하고 있기 때문에, 애플리케이션 측에서IME를 제어하는 수단이 없기 때문입니다. | |
HTML의TextField등으로 대체하는 등으로 대처해야합니다. | |
----- 랜덤 2개 뽑아야 할경우 간단한 방법 (3개 이상은 안댐) ------ | |
가끔 프로그래밍을 하다가 랜덤한 숫자(혹은 다른 어떤것)이 필요할 때가 있습니다. | |
한개만 뽑으려면 rand()%<원하는 범위> 처럼 뽑으면 되지만 여러개를 뽑을때는 상황이 약간 달라집니다. | |
예를 들어 어떤 그룹에서 두명을 뽑아 승부를 겨루는 일이 있다고 해봅시다. | |
rand함수를 두번 돌리면 같은사람이 두번 뽑혀서 문제가 발생합니다.(자기자신과 승부를 하는건 의미가 없죠) | |
사실 여기까지는 간단합니다. | |
보통은 이렇게 하지요 | |
player1=rand()%(전체_인원_수); | |
do player2=rand()%(전체_인원_수) while player1==player2 | |
하지만 이렇게 하면 언제 끝날지 모릅니다.(큰 배열에서 2명이라면 거의 한번에 끝나지만 이런식으로 여러명을 뽑을때는 운이 안좋으면 시간이 엄청 걸립니다.) | |
불필요하게 다시 랜덤을 돌리는 일을 방지하려면 이렇게 하면 됩니다. | |
player1=rand()%(전체_인원_수); | |
player2=rand()%(전체_인원_수 - 1); | |
if(player2>=player1) player2+=1; | |
------------- WebGL 빌드 테스팅 ------------ | |
크롬에서 file:// 직접 불러오면 안되더라. (2019.02기준) | |
파이어폭스에서 테스트해봐야함 아님 서버에 올려서 하거나 | |
------------- 유니티 스프라이트 마스크 ---------------- | |
스프라이트 마스크를 사용하면 SpriteRenderer에서 InsideMask, OutSideMask 해두면 | |
Sprite Mask 컴포넌트 단 오브젝트, 마스크용 Sprite 이용해서 특정 부분만 보이게 하거나 안보이게 할 수 있다. | |
이거 잘 이용하면 이미지 리소스 낭비도 줄일 수 있고, 여러가지 용도로 사용할 수 있다. | |
코드 정리 PRESET
#region VARIABLES & PROPERTIES
#endregion VARIABLES & PROPERTIES
#region UNITY_EVENTS
#endregion UNITY_EVENTS
#region MAIN_FUNCTIONS
#endregion MAIN_FUNCTIONS
#region SUB_FUNCTIONS
#endregion SUB_FUNCTIONS
#region UI_FUNCTIONS
#endregion UI_FUNCTIONS
#region COROUTINE_FUNCTIONS
#endregion COROUTINE_FUNCTIONS
폴더 정리 PRESET
01Scene
02Script
03Prefab
04Image
05Audio
06Animation
07Material
08Particle
09Font
10Shader
11Model
하이라키 정리 PRESET
CAMERA
UI
MAP
OBJECT
MANAGER
#region DATA
#endregion DATA
#region VARIABLES & PROPERTIES
#endregion VARIABLES & PROPERTIES
#region INIT_FUNCTIONS
#endregion INIT_FUNCTIONS
#region MAIN_FUNCTIONS
#endregion MAIN_FUNCTIONS
#region SUB_FUNCTIONS
#endregion SUB_FUNCTIONS
#region EVENT_FUNCTIONS
#endregion EVENT_FUNCTIONS
이 모든 내용은 오늘도 혼자 고민하고 거북목을 감싸고 있는 모든 개발자들에게 바칩니다.
찾거나 발견한 것들 제 생각들을 메모한 내용이므로 정확하지 않을 수 있으며, 자유롭게 가져다 쓰시고 출처도 남길 필요 없습니다.
GNU GNU GNU
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
질문 Sprite Mask 2중 중첩일때
https://answers.unity.com/questions/1699002/mask-a-sprite-with-multiple-spritemasks.html