처음 3*3 블럭이 사라지는 효과가 나오고 마우스로 클릭하면 1*1 크기의 블럭이 사라지는 효과를 만들어 봤습니다.
먼저 최종 완성된 영상을 봐야 이해가 쉬울 겁니다.
2. 전체 구조
들어가기에 앞서 Object의 전체 구조에 대해서 알아 보겠습니다.
2.1 MouseInput
마우스를 클릭한다면 ImpactPoint Object를 만드는 Object입니다.
2.2 ImpactPoint
어떤 블럭이 깨질지 깨지는 블럭을 위치를 설정하고 깨지는 위치 마다 TileSplitter Object를 만듭니다. 그리고 해당 블럭을 타일맵에서 삭제합니다.
2.3 TileSplitter
타일을 Mask된 Image여러개 쪼개서 여러개의 BrokenSprite들을 만드는 역할을 합니다.
2.4 BrokenSprite
스프라이트 마스크를 통해서 Sprite를 그리고 일정 시간 후에 Fadeout 시킵니다.
3. 코드 설명
3.1 MouseInput
MouseInput에서는 앞에서 ImpactPoint Object를 생성 시킨다고 하였습니다. Impact Point Prefab 인자를 두었습니다. 그리고 Destructible Tile Map은 파괴가 가능한 타일맵을 설정합니다. 타일맵의 Layer를 여러개 두고 파괴가 안되는 처리도 가능합니다.
publicclassMouseInput : MonoBehaviour
{
Vector3 mousePosition;
public GameObject impactPointPrefab;
public Tilemap destructibleTileMap;
// Update is called once per framevoidUpdate()
{
if (Input.GetMouseButtonDown(0))
{
mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
GameObject obj = Instantiate(impactPointPrefab, mousePosition, Quaternion.identity, transform);
obj.GetComponent<ImpactPoint>().defaultRadius = new Vector2Int(0, 0);
obj.GetComponent<ImpactPoint>().destructibleTileMap = destructibleTileMap;
}
}
}
스크립트 내용은 간단합니다. 마우스를 클릭하면 impactPointPrefab 을 생성하고 ImpactPoint Object에 몇가지 정보를 연결합니다.
3.2 ImpactPoint
ImpactPoint 의 경우 TileSplitter를 생성하기 때문에 정보가 있어야 하고 파괴할 TileMap정보인자도 들어 있습니다. Prefab에서 설정을 할 수가 없기 때문에 None 상태입니다. 이건 MouseInput 당시에 object생성 후 설정을 하도록 구현 하였습니다.
usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
usingUnityEngine.Tilemaps;
publicclassImpactPoint : MonoBehaviour
{
privateint updateTimer = 0;
publicint maxTime;
public Vector2Int defaultRadius;
public Tilemap destructibleTileMap;
public GameObject tileSplitterPrefab;
private List<Vector3Int> tilesToBreak;
// Start is called before the first frame updatevoidStart()
{
tilesToBreak = GetAffectedTiles(transform.position, defaultRadius);
}
privatevoidFixedUpdate()
{
if (tilesToBreak.Count > 0)
{
if (updateTimer < maxTime) updateTimer++;
else
{
List<Vector3Int> tilesToRemoveFromList = new List<Vector3Int>();
List<TileSplitter> convertedTiles = new List<TileSplitter>();
foreach (var tile in tilesToBreak)
{
Vector3Int position = tile;
position.z = (int)destructibleTileMap.transform.position.z;
if (destructibleTileMap.GetTile(position))
{
if (!tilesToRemoveFromList.Contains(position))
{
GameObject newTile = Instantiate(tileSplitterPrefab, position, Quaternion.identity);
TileSplitter handler = newTile.GetComponent<TileSplitter>();
handler.tileSprite = destructibleTileMap.GetSprite(position);
convertedTiles.Add(handler);
destructibleTileMap.SetTile(position, null);
tilesToRemoveFromList.Add(position);
}
}
else tilesToRemoveFromList.Add(position);
}
foreach (var tile in tilesToRemoveFromList)
{
tilesToBreak.Remove(tile);
}
tilesToRemoveFromList.Clear();
updateTimer = 0;
}
}
elseDestroy(this.gameObject);
}
private List<Vector3Int> GetAffectedTiles(Vector2 impactPosition, Vector2Int impactRadius)
{
List<Vector3Int> allTiles = new List<Vector3Int>();
Vector3 startPosition = impactPosition;
startPosition = new Vector3(impactPosition.x, impactPosition.y, 0);
for (int x=-impactRadius.x;x<=impactRadius.x;x++)
{
for (int y = -impactRadius.y; y <= impactRadius.y; y++)
{
Vector3Int pos1 = destructibleTileMap.WorldToCell(startPosition + new Vector3(x, y, 0));
if (destructibleTileMap.GetTile(pos1) && !allTiles.Contains(pos1)) allTiles.Add(pos1);
Vector3Int pos2 = destructibleTileMap.WorldToCell(startPosition - new Vector3(x, y, 0));
if (destructibleTileMap.GetTile(pos2) && !allTiles.Contains(pos2)) allTiles.Add(pos2);
}
}
return allTiles;
}
}
GetAffectedTiles 함수는 실제 어떤 블럭을 깰지 계산하는 함수 입니다.
1*1 블럭에서는 하나밖에 없지만 impactRadius의 크기가 주어지면 좀 더 많은 블럭이 깨져야 하기 때문입니다.
maxTime이라는 부분이 보이는데 해당 변수는 위 코드에 delay를 주기위한 장치라고 보면 됩니다.
전체적으로 코드는 TileSplitter 생성하고 인자를 넘겨주는 부분으로 되어있습니다.
3.3 TileSplitter
여기에서는 타일 마스크라는 이미지가 필요한데 검은색이미지를 구분을 지어서 타일 크기에 맞게 각각 만들었습니다. 여기에서는 5조각으로 분리되도록 작업하였기 때문에 5장의 이미지를 그렸습니다.
usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassTileSplitter : MonoBehaviour
{
public Sprite tileSprite;
public PhysicsMaterial2D physicsMaterial;
[SerializeField]private GameObject prefab;
[SerializeField]private GameObject parentPrefab;
[SerializeField]private List<Sprite> spriteMasks;
// Start is called before the first frame updatevoidStart()
{
BreakSpriteToMasks();
}
privatevoidBreakSpriteToMasks()
{
List<GameObject> newObjects = new List<GameObject>();
GameObject parent = Instantiate(parentPrefab, transform.position, Quaternion.identity);
for(int i = 0;i<spriteMasks.Count; i++)
{
GameObject newGO = Instantiate(prefab, transform.position+new Vector3(0.5f,0.5f,0),Quaternion.identity);
newObjects.Add(newGO);
}
UpdateSpriteObject(parent, newObjects, spriteMasks, tileSprite, physicsMaterial);
Destroy(gameObject);
}
privatevoidUpdateSpriteObject(GameObject parent, List<GameObject> newGO, List<Sprite> sprites, Sprite tileSprite, PhysicsMaterial2D physicsMaterial)
{
for (int i = 0; i < newGO.Count; i++)
{
newGO[i].GetComponent<Rigidbody2D>().sharedMaterial = physicsMaterial;
newGO[i].GetComponent<SpriteRenderer>().sprite = sprites[i];
newGO[i].GetComponent<PolygonCollider2D>();
newGO[i].GetComponent<PolygonCollider2D>().sharedMaterial = physicsMaterial;
UpdateShapeToSprite(newGO[i].GetComponent<PolygonCollider2D>());
newGO[i].transform.GetChild(0).GetComponent<SpriteRenderer>().sprite = tileSprite;
newGO[i].transform.GetChild(0).GetComponent<SpriteMask>().sprite = sprites[i];
newGO[i].transform.parent = parent.transform;
newGO[i].transform.localScale *= 0.95f;
}
}
privatevoidUpdateShapeToSprite(PolygonCollider2D collider)
{
Sprite sprite = collider.GetComponent<SpriteRenderer>().sprite;
if (collider != null && sprite != null)
{
collider.pathCount = sprite.GetPhysicsShapeCount();
List<Vector2> path = new List<Vector2>();
for (int i = 0; i < collider.pathCount; i++)
{
path.Clear();
sprite.GetPhysicsShape(i, path);
collider.SetPath(i, path.ToArray());
}
}
}
}
깨트릴 sprite block 이미지위에 mask를 씌워서 여러장을 만든형태입니다. localScale도 약간 작게 조절합니다. 그리고 물리 충돌을 넣어주면 좀 더 자연스럽게 부서집니다. 물리 충돌 설정을 위한 코드가 UpdateShapeToSprite() 함수 입니다.
여기에서 DParent 가 있는데 자식들 관리를 위한 부모 dummy prefab이라고 보시면 됩니다.
3.4 BrokenSprite
BrokenSprite 는 아래 VisualSprite 이름의 Sprite Object를 가지고 있습니다.
BrokenSprite 의 Color은 알파값을 0 설정을 해서 투과 설정을 해줍니다. Mask된 이미지는 VisualSprite에서 출력을 하기 때문입니다.
Mask Interaction 설정은 Visible inside Mask 로 설정을 해줍니다. 그리고 Sprite Mask 컴포넌트도 추가해줍니다.
usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassBrokenSpriteFadeOutHandler : MonoBehaviour
{
[SerializeField]privatefloat fadeSpeed;
[SerializeField]privateint destroyDistance;
[SerializeField]privateint forceFadeTimer;
private Rigidbody2D rb;
privatebool startFade;
privateint forceFade;
// Start is called before the first frame updatevoidStart()
{
rb = GetComponent<Rigidbody2D>();
transform.Rotate(0,0,90*Random.Range(0,4));
}
privatevoidLateUpdate()
{
float distanceToCamera = Vector2.Distance(transform.position, Camera.main.transform.position);
if (distanceToCamera > destroyDistance) Destroy(gameObject);
elseif (startFade)
{
Color colour = transform.GetChild(0).GetComponent<SpriteRenderer>().color;
colour.a -= fadeSpeed;
transform.GetChild(0).GetComponent<SpriteRenderer>().color = colour;
if (colour.a <=0)Destroy(gameObject);
}
else
{
if (rb != null && rb.velocity.x == 0) startFade = true;
if (forceFade < forceFadeTimer) forceFade++;
else startFade = true;
}
}
// Update is called once per framevoidUpdate()
{
}
}
https://gongu.copyright.or.kr/ 여기에서 쓸만한 폰트를 다운로드 받습니다.
학교안심 우주 R 이라는 폰트를 다운로드 받았습니다.
2. 폰트 파일을 Asset 폴더에 넣습니다.
3. 필요시 적절한 폴더로 이동합니다.
여기에서는 Font 폴더를 만들어서 이동하였습니다.
4. 폰트 에셋 만들기 메뉴를 선택합니다.
5. 폰트 아틀라스 생성하기
아틀라스란 스프라이트를 하나의 이미지로 만드는 작업입니다. 아틀라스를 만듦으로서 성능향상(드로우 콜이 작아짐)을 가져올 수 있기 때문에 비슷하게 전체 폰트에 대해서도 하나의 아틀라스 이미지를 생성하게 됩니다.
Sampling Point Size가 커지면 이미지 품질이 좀 더 좋아지게 되나 더 큰 아틀라스 이미지의 크기가 필요하게 됩니다.
여기 설명 안된건 기본값으로 사용합니다.
- SourceFontFile : 다운받은 폰트 선택
- Sampling Point Size : 자동으로 셋팅하거나 사이즈 지정해줌 아래에서는 32를 사용하였으나 해당 크기는 주로 사용하는 폰트의 크기와 비슷한 크기를 지정해 줍니다.
- Atlas Resolution : 아틀라스 크기인데 적절한 값을 정해줍니다. 일반적으로 4096 * 4096을 많이들 사용합니다만, 모바일 환경을 고려한다면 가능하면 작게 설정이 필요합니다. (너무 작게 설정시 한장의 아틀라스에 표현이 안될 수 있으니 확인 방법은 아래 설명을 참고 바랍니다.)
- CharacterSet : CustomRange
- Character Sequence 입력 : 32-126,44032-55203,12593-12643,8200-9900 한글에서는 일반적으로 이 구간의 문자만 사용합니다.
만약 설정을 아틀라스 크기가 작거나 샘플링 크기가 크거나 등등 모든 폰트를 하나의 아틀라스 이미지에 표현하지 못한다면 위 스크린샷 마지막 항목 Excluded characters:0 이라고 표기된 부분에 0이 아닌 숫자가 나타나게 됩니다. 해당 경우에는 숫자 값을 조절해야 합니다.
유명한 젠가라는 게임과 무너질때까지 진행한다는 점에서는 비슷하면서도 게임 방식은 반대가 되는 게임입니다. 블럭을 떨어뜨리면서 무너지지 않을때까지 진행하는 게임입니다.
여기에서는 2D형태로 Unity를 이용하여 제작하였습니다.
1. 라이센스
CC0
모든 코드는 자유롭게 수정 배포할 수 있습니다.
2. 동작 영상
3. 이미지
4. 떨어지는 블럭 만들기
block Image를 Scene로 Drag & Drop 한뒤 다시 해당 오브젝트를 Assets으로 가져오면 Prefab Asset이 만들어 집니다.
그리고 나서 몇가지 콤포넌트를 추가했습니다.
4.1 컴포넌트들
Transform, Sprite Renderer는 기본으로 추가 되고 Rigidbody 2D, Box Collider 2D 는 물리 게임에서는 필수라서 추가가 되었고 DownBlock 이라는 스크립트를 추가 했습니다.
대부분 기본값을 사용하였으며 Rigidbody 2D 에서는 움직이는 물체라서 Dynamic으로 변경하였습니다.
4.2 DownBlock.cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
publicclassDownBlock : MonoBehaviour
{
Transform tf = null;
Rigidbody2D rg = null;
publicfloat maxScale = 3.0f;
publicbool rightMove = true;
publicfloat moveSpeed = 4.0f;
publicbool drop = false;
publicbool needCheck = false;
publicstaticstring gameState = "playing";
// Start is called before the first frame updateprivatevoidAwake()
{
tf = GetComponent<Transform>();
tf.localScale = new Vector3(0f, 0f, 0f);
rg = GetComponent<Rigidbody2D>();
rg.gravityScale = 0f;
drop = false;
needCheck = false;
}
// Update is called once per framevoidUpdate()
{
if (gameState == "gameend")
{
GetComponent<Rigidbody2D>().gravityScale = 1f;
drop = true;
}
if (tf.localScale.x >= maxScale && drop==false && (Input.GetKey(KeyCode.Space) || Input.GetMouseButton(0)))
{
GetComponent<Rigidbody2D>().gravityScale = 1f;
drop = true;
}
}
voidFixedUpdate()
{
Vector3 scale = tf.localScale;
bool havetomove = false;
if (!drop)
{
if (scale.x < maxScale)
{
scale.x = scale.x + 8f * Time.fixedDeltaTime;
scale.y = scale.x;
}
else
{
scale.x = maxScale;
scale.y = scale.x;
havetomove = true;
}
tf.localScale = new Vector3(scale.x, scale.y, 0f);
if (havetomove)
{
Vector3 pos = rg.position;
if (rightMove)
{
pos.x = pos.x + moveSpeed * Time.fixedDeltaTime;
}
else
{
pos.x = pos.x - moveSpeed * Time.fixedDeltaTime;
}
rg.MovePosition(new Vector3(pos.x, pos.y, pos.z));
}
}
}
privatevoidOnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Wall"))
{
if (rightMove)
{
rightMove = false;
}
else
{
rightMove = true;
}
}
elseif (collision.gameObject.CompareTag("DeadWall"))
{
gameState = "gameover";
Destroy(gameObject);
}
}
privatevoidOnTriggerExit2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("DontCare"))
{
needCheck = true;
}
}
}
4.3 변수 설명
maxScale : pixel로 만들다 보니 크기가 작습니다. 화면에 너무 작게 나와서 확대해서 표현이 되는데 적절한 크기인 3.0로 정했습니다.
또한 처음 신규 block이 나타날때는 크기를 0에서부터 시작해서 3.0까지 scale을 증가시켜서 자연스럽게 등장 하는 효과를 보기 위한 용도도 있습니다.
rightMove : 생성된 벽은 최초 해당값에 따라 이동이 됩니다. rightMove가 true다면 처음부터 오른쪽으로 이동하다가 벽에 막히면 방향을 바꾸어서 이동하게 하는 변수 입니다.
moveSpeed : Block의 이동 속도입니다.
drop : 사용자가 키를 눌러서 아래로 떨어지는 블럭임을 표시하는 변수 입니다.
needCheck : 블럭이 떨어지면 블럭이 움직이지 않아야만 다음 블럭이 나타나게 됩니다. 처음 생성된 블럭은 x, y 좌표로 움직을 주기 때문에 속도가 나타나지 않습니다. drop을 시작해야만 속도가 발생하게 됩니다. 그래서 초기에 생성된 속도가 없는 블럭들은 검사대상에서 제외하기 위해 만들어진 변수 입니다. true 인 블럭만 블럭이 멈췄는지 검사를 하게 됩니다.
gameState : 게임이 진행중인지 종료인지 상태를 나타냅니다. static 변수라서 전 class에서 하나의 영역에만 존재하는 값입니다.
OnTriggerExit 이벤트에 해당 내용을 추가하여 "DontCare" tag 영역을 블럭이 벗어날때 needCheck 값을 true로 변경하였습니다.
위 스크린샷에서 빨간색 사각형이 DontCare Tag 블럭의 위치입니다.
5. 플랫폼 설치
떨어지는 블록을 받아줄 단단한 바닥을 만들 차례입니다.
platform 이미지를 끌어와서 Platform 이라는 이름을 붙여줍니다. 그리고 적당한 위치에 옮겨 놓습니다.
위와 같이 처리하고 Rigidbody 2D 컴포넌트를 추가하고 Body Type을 Static으로 변경합니다. 추가로 Box Collider 2D도 추가해줍니다.
6. Egg 포인트
spawn 이나 egg로 표현하는데 어떤 물체가 생성하는 장소를 말합니다. 이 게임에서는 다음 block이 생성되는 장소로 정의 하였습니다. 위치만 사용할 예정이므로 다른 컴포넌트는 필요없습니다.
대략 이 정도 위치에 아무것도 없이 추가하였습니다. 추가시 주의할점은 Main Camera 자식위치로 설정하였습니다. 또한 구별하기 쉽게 특별히 아이콘도 넣어주었습니다.
7. GameManager
EmptyObject를 생성 후 이름을 변경해서 스크립트를 GameManager라는 이름으로 만들었습니다. 이곳에서 관리를 위한 여러가지 처리를 담당하도록 구현하였습니다.
7.1 GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;
publicclass GameManager : MonoBehaviour
{
publicstatic GameManager instance;
public GameObject genObj;
public GameObject startPanel;
public GameObject gameoverPanel;
public GameObject scoreText;
public Transform genPosition;
publicfloat invokeTimer = 1.0f;
int score = 0;
float mainCamTargetY = 0f;
// Start is called before the first frame updatevoid Start()
{
instance = this;
DownBlock.gameState = "playing";
if (gameoverPanel)
{
gameoverPanel.SetActive(false);
}
if (invokeTimer > 0f)
{
Invoke("InvokeFunction", invokeTimer);
Invoke("InvokeStartImageDeActive", invokeTimer);
}
score = 0;
}
// Update is called once per framevoid Update()
{
if (DownBlock.gameState == "playing")
{
GameObject[] block = GameObject.FindGameObjectsWithTag("Player");
float maxVelocy = -1f;
float maxY = -100f;
int dropFalseCount = 0;
int blockCount = 0;
for (int i = 0; i < block.Length; i++)
{
DownBlock downBlock = block[i].GetComponent<DownBlock>();
Rigidbody2D rd = block[i].GetComponent<Rigidbody2D>();
if (rd != null)
{
if (!downBlock.drop)
{
dropFalseCount++;
continue;
}
elseif (!downBlock.needCheck)
{
dropFalseCount++;
continue;
}
else
{
if (maxVelocy < rd.velocity.magnitude)
{
maxVelocy = rd.velocity.magnitude;
}
if (maxY < rd.position.y)
{
maxY = rd.position.y;
}
blockCount++;
}
}
}
GameObject mainCam = GameObject.FindGameObjectWithTag("MainCamera");
if (block.Length > 0 && dropFalseCount == 0 && maxVelocy < 0.1f)
{
{
InvokeFunction();
if (maxY + 2 > mainCam.transform.position.y)
{
mainCamTargetY = maxY + 2;
}
score = blockCount;
}
}
if (mainCamTargetY > mainCam.transform.position.y)
{
mainCam.transform.position = new Vector3(mainCam.transform.position.x, mainCam.transform.position.y + 0.5f * Time.deltaTime, mainCam.transform.position.z);
}
}
elseif (DownBlock.gameState == "gameover")
{
if (gameoverPanel)
{
gameoverPanel.SetActive(true);
}
}
if (scoreText)
{
scoreText.GetComponent<Text>().text = score.ToString();
}
}
void InvokeFunction()
{
GameObject result;
result = Instantiate(genObj);
result.transform.position = genPosition.transform.position;
}
void InvokeStartImageDeActive()
{
if (startPanel)
{
startPanel.SetActive(false);
}
}
}
7.2 변수 설명
public static GameManager instance; // 다른곳에서 GameManager 변수를 쉽게 접속하기 위해서 만들어 뒀는데 이번 예제에서는 사용하지 않았습니다.
public GameObject genObj; // 생성을 위한 block ( Prefab 된 block 을 선택해줍니다.) 을 의미합니다.
public GameObject startPanel; // 게임을 시작하면 Start Game이라는 글자가 나오는 UI 판넬입니다.
public GameObject gameoverPanel; // 게임이 종료될때 나타나는 Game over 판넬입니다. 이곳에는 재시작 버튼이 포함되어 있습니다.
public GameObject scoreText; // 상단에 점수를 출력하는 UI 입니다.
public Transform genPosition; // 생성되는 위치 즉, Egg 포인트를 의미합니다.
public float invokeTimer = 1.0f; // 게임을 시작하면 곧장 시작하는것이 아닌 일정한 간격을 두고 시작하는데 게임 시작 delay를 주기위한 시간입니다.
int score = 0; // 현재 점수를 나타냅니다.
float mainCamTargetY = 0f; // 카메라 Y 위치를 조정하기 위한 변수입니다. 여기에 설정된 값을 기준으로 카메라가 해당값과 같아질때까지 조금씩 이동을 합니다. (MainCamera는 최초 0위치에 있기 때문에 해당값도 0이 됩니다.)
Pulbic 변수를 어떻게 할당했는지 위 Screenshot 확인하기 바랍니다.
7.3 코드 설명
시작 코드는 아래와 같이 특정 조건이 맞을때 초기화 하는 것으로 구성되어 있습니다.
처음 시작시 gameoverPanel 은 있다면 SetActive(false) 로 꺼주도록 합니다.
그리고 gameState도 playing 상태로 만들어줄 필요가 있습니다.
void Start()
{
instance = this;
DownBlock.gameState = "playing";
if (gameoverPanel)
{
gameoverPanel.SetActive(false);
}
if (invokeTimer > 0f)
{
Invoke("InvokeFunction", invokeTimer);
Invoke("InvokeStartImageDeActive", invokeTimer);
}
score = 0;
}
1초 후에 startPanel 과 Prefab에 있는 block을 생성시키는 동작을 합니다.
8. StartPanel
StartPanel은 UI>Panel 을 이용해서 만들었습니다.
그리고 아래에 UI>Image 를 이용해서 이미지를 넣었습니다.
1초 후에 SetActive(false) 이 호출되어 이미지가 사라지게 됩니다.
생성된 object는 result로 들어오고 계층도에서 최상위에 생성이 됩니다. (BlockEgg 자식으로 생성되는것이 아닙니다.) result.transform.position = genPosition.transform.position; 이 코드를 이용해서 위치만 해당 위치로 이동시켜 줍니다.
실제 동작되는 모습에서 확인이 가능합니다.
이와 같이 구현한 이유는 BlockEgg가 Main Camera 하위에 있기 때문에 Camera 이동시 문제가 발생하기 때문입니다.
8.1 Update함수
해당 함수내에 코드는 block 이 이동이 멈췄다면 다음 블럭이 나오도록 하는 코드와 점수 계산 카메라 이동을 담당합니다.
GameObject[] block = GameObject.FindGameObjectsWithTag("Player");
float maxVelocy = -1f;
float maxY = -100f;
int dropFalseCount = 0;
int blockCount = 0;
for (int i = 0; i < block.Length; i++)
{
DownBlock downBlock = block[i].GetComponent<DownBlock>();
Rigidbody2D rd = block[i].GetComponent<Rigidbody2D>();
if (rd != null)
{
if (!downBlock.drop)
{
dropFalseCount++;
continue;
}
elseif (!downBlock.needCheck)
{
dropFalseCount++;
continue;
}
else
{
if (maxVelocy < rd.velocity.magnitude)
{
maxVelocy = rd.velocity.magnitude;
}
if (maxY < rd.position.y)
{
maxY = rd.position.y;
}
blockCount++;
}
}
}
위 코드는 모든 블럭을 찾아서 drop true 이고 needCheck 인 블럭들만 모아서 blockCount 를 하게 되면 그것이 점수가 됩니다. 또한 해당 블럭들이 Rigidbody2D 의 velocity.magnitude를 산출해냅니다. 이 값을 이용해서 블럭이 멈췄는지 알아 내게 되는데 모든 block중에 가장 큰값을 알아내고 y 좌표도 가장 큰 값을 알아내는 코드입니다.
코드는 게임 종료시는 SetActive(true)를 해주고 게임 시작시는 SetActive(false) 로 처리합니다.
9. Scene 이동
UI>Legacy>Button 을 이용해서 만든 버튼에 Scene 이동 동작을 넣는 방법입니다.
조금 복잡할 수 있는데 ChangeScene.cs 스크립트를 만들고 해당 버튼에 스크립트 컴포넌트를 연결하고 On Click() 부분에 RestatButton을 끌어와서 놓아야 Load() 함수가 나타나게 됩니다. 컴포넌트 인자는 이동하고자하는 Scene를 넣습니다. 이 게임은 Scene 이름은 다음과 같기 때문에 StackPlayScene 값을 입력 하였습니다.
9.1 ChangeScene.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
publicclassChangeScene : MonoBehaviour
{
publicstring sceneName;
publicvoidLoad()
{
SceneManager.LoadScene(sceneName);
}
}
10. 스코어 표기
text object는 UI>Legacy>Text로 만들어 주면 됩니다.
if (scoreText)
{
scoreText.GetComponent<Text>().text = score.ToString();
}