2024년 1월 24일 수요일

GameObject (creation order) 생성 순서에 대한 고찰

#unity change order GameObject

 Unity 를 사용하면서 Awake나 Start 함수에서 다른 GameObject 상태를 살피는 경우가 종종 있습니다.

그런데 간혹 잘되던 상태가 종종 오동작하는 경우가 있어서 Object 생성 순서와 관계가 있을것 같아서 조사해 보았습니다.

구글링을 했을때는 마땅한 결론이 없어서 이것 저것 테스트 해보았습니다.

Hierarchy 에서 게임 Object를 순서대로 만들었습니다. 이름은 혹시 몰라 A,B,C,1,2,3,c,b,a 순서로 지었습니다.

그리고 Test Code에서는 Awake 진입하면 Object 이름이 출력되도록 로그를 넣었습니다.


실제 테스트를 하게되면 아래와 같은 형태로 나옵니다.

순서는 오래전 생성된것이 처음에 나오고 마지막에 추가된 Obejct가 마지막에 나옵니다.

Hierarchy 에서 순서를 마구 섞어 보았습니다.


순서는 바뀌지 않았습니다.


혹시 몰라서 Alphanumeric Sorting 기능도 건들어봤습니다.


전혀 변화가 없습니다.


지금부터는 순서를 변화 하는 방법입니다.

동일한 기능을 하는 새 스크립트를 만들어서 추가했습니다.

수정된 Object가 가장 처음으로 올라오는 변화가 일어납니다.

그렇다면 여기에서 다시 예전 스크립트로 돌린다면 순서가 예전으로 돌아가는지 확인해 보았습니다.


한번 변화된 순번은 원복되지 않습니다.

이번에는 script component 를 삭제했다가 다시 추가해 보았습니다.


처음으로 올라갑니다.



결론 : 변화가 있는 GameObject가 제일 마지막에 생성됩니다. (여기에서는 컴포넌트 변화를 주었습니다.)
순서를 변경하려면 최악의 경우 위와 같이 해야하지만, 실제는 Object 생성 순서에 영향이 없도록 Awake와 Start를 조합해서 잘 설계하는 것을 추천합니다.



2024년 1월 12일 금요일

DOTween Time.timeScale = 0 인 경우 동작하는 방법, SetUpdate(true) 추가해도 동작이 안될때

 DOTween 에서 Time.timeScale = 0 일때 animation 을 사용 하는 방법

동작 뒤에 SetUpdate(true추가해 주면 됩니다.

그런데 추가해도 안되는 경우가 있었습니다.

아래와 같은 예제가 있습니다.

            var seq = DOTween.Sequence();
            
            transform.localScale = Vector3.one * 0.1f;

            seq.Append(transform.DOScale(1.1f, fadeTime).SetUpdate(true));
            seq.Append(transform.DOScale(1f, 0.1f).SetUpdate(true));

            seq.Play().OnComplete(() =>
            {
                transform.localScale = Vector3.one;
            });

어디가 잘못 되었는지 찾았나요?



수정한 코드입니다.

        var seq = DOTween.Sequence();
            
        transform.localScale = Vector3.one * 0.1f;

        seq.Append(transform.DOScale(1.1f, fadeTime));
        seq.Append(transform.DOScale(1f, 0.1f));

        seq.Play().SetUpdate(true).OnComplete(() =>
        {
            transform.localScale = Vector3.one;
        });

sequence 를 사용하고 있다면 Play() 에 SetUpdate(true)를 넣어야 합니다.




2024년 1월 8일 월요일

requests post 에서 streaming 사용방법

requests streaming 의 필요성

requests에서 streaming이란 조금씩 buffering 하는것을 의미합니다. 일반적으로 파일을 upload하게 된다면 requests 모듈안에서는 파일 통째로 메모리에 올려서 upload하게 됩니다.

만약 파일의 크기가 크다면 어떻게 될까요? 많은 메모리가 필요하게 됩니다. 심지어는 메모리 부족 사태까지 발생될 수 있습니다.


requests streaming 사용법

가이드 문서에서는 아래 링크를 참고 하면 됩니다.

https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads

Streaming Uploads

Requests supports streaming uploads, which allow you to send large streams or files without reading them into memory. To stream and upload, simply provide a file-like object for your body:

with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

Warning

It is strongly recommended that you open files in binary mode. This is because Requests may attempt to provide the Content-Length header for you, and if it does this value will be set to the number of bytes in the file. Errors may occur if you open the file in text mode.

위 용법을 보면 크게 기존 사용하는 방법과 큰 차이가 없어보입니다.

그렇다면 이미 streaming 하고 있는걸까요?

실제 테스트해보니 동작하고 있지 않았습니다.


코드 안쪽으로 디버깅 해보기

일반적으로 requests를 사용해서 파일을 업로드시 아래와 같은 방식으로 사용하게 됩니다.

https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file

POST a Multipart-Encoded File

Requests makes it simple to upload Multipart-encoded files:

>>> url = 'https://httpbin.org/post'
>>> files = {'file': open('report.xls', 'rb')}

>>> r = requests.post(url, files=files)
>>> r.text
{
  ...
  "files": {
    "file": "<censored...binary...data>"
  },
  ...
}

차이가 뭘까요? 

정답은 data=f , files=files 넘어가는 인자가 틀립니다. 

그렇습니다. 

!중요! requests에서 files로 넘어가는 인자에 대해서는 streaming 을 지원하지 않습니다.

엥 이게 무슨말이냐고요? 거짓말 아닌지 문의하실것 같은데요.

코드에서 로그를 넣어서 확인이 가능했습니다.

requets/models.py라는 코드를 살펴보다보면 아래와 같은 부분이 보입니다.

def prepare_body(self, data, files, json=None):
"""Prepares the given HTTP body data."""

# Check if file, fo, generator, iterator.
# If not, run through normal process.

# Nottin' on you.
body = None
content_type = None

if not data and json is not None:
# urllib3 requires a bytes-like body. Python 2's json.dumps
# provides this natively, but Python 3 gives a Unicode string.
content_type = "application/json"

try:
body = complexjson.dumps(json, allow_nan=False)
except ValueError as ve:
raise InvalidJSONError(ve, request=self)

if not isinstance(body, bytes):
body = body.encode("utf-8")

is_stream = all(
[
hasattr(data, "__iter__"),
not isinstance(data, (basestring, list, tuple, Mapping)),
]
)

if is_stream:
try:
length = super_len(data)
except (TypeError, AttributeError, UnsupportedOperation):
length = None

body = data

if getattr(body, "tell", None) is not None:
# Record the current file position before reading.
# This will allow us to rewind a file in the event
# of a redirect.
try:
self._body_position = body.tell()
except OSError:
# This differentiates from None, allowing us to catch
# a failed `tell()` later when trying to rewind the body
self._body_position = object()

if files:
raise NotImplementedError(
"Streamed bodies and files are mutually exclusive."
)


    is_stream = all(
[
hasattr(data, "__iter__"),
not isinstance(data, (basestring, list, tuple, Mapping)),
]
)

is_stream 이 True되는 조건자체가 data인자만 확인하도록 되어 있습니다. 즉 streaming은 file쪽은 확인하지도 않게되고, 만약 is_stream 으로 들어가서도 files가 존재하게된다면 아래 코드에 의해서 exception이 발생하도록 되어있습니다.

        if files:
raise NotImplementedError(
"Streamed bodies and files are mutually exclusive."
)


해결 방법

이미 다른 분들이 구현해 놓은 모듈이 존재합니다.

https://github.com/requests/toolbelt

사용법은 files로 넘어가는 부분을 data로 옮기는 작업을 해야합니다.

github의 예제를 참고해서 data+files를 MultipartEncoder 넣어서 작업하면 됩니다.

from requests_toolbelt import MultipartEncoder
import requests

m = MultipartEncoder(
    fields={'field0': 'value', 'field1': 'value',
            'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
    )

r = requests.post('http://httpbin.org/post', data=m,
                  headers={'Content-Type': m.content_type})



2024년 1월 2일 화요일

unity background scrolling

Unity에서 백그라운드 스크롤을 할 수 있는 방법입니다.

여러장의 이미지를 이용해서도 가능하지만 여기에서는 Material 을 이용해서 하는 방법을 정리하였습니다. 실제 따라해 보니 동작이 안되던 부분도 있고 해서 여기 저기 내용을 합쳐서 정리해 보았습니다.

2D 기준으로 테스트 하였습니다.


1. 이미지 준비

스클롤을 하려면 적당한 이미지가 필요로 합니다.

좌우로 스크롤 하려면 당연히 좌우측 이미지가 연결되어야 합니다.



2. Unity 에서 이미지 Wrap Mode에서 Repeat 설정


3. Create -> Meterial 을 만들고 Mobile/Particles/Alpha Blended 설정을 합니다.


4. 1에서 준비한 background 이미지를 추가합니다.

5. 4번에서 만든 이미지에 Meterial 적용합니다.

Sprite Renderer 항목중 Meterial 항목이 있습니다. 거기에 3번에서 생성한 파일을 Drag&Drop 해주면 추가가 됩니다.


6. 코드 작업을 해서 컴포넌트에 추가합니다.

using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UIElements;
 
public class ScrollBG : MonoBehaviour
{
    public float scrollspeed = 0.2f;
    public bool isScrollX = true;
    Material material;
 
 
    private void Start()
    {
        material = GetComponent<Renderer>().material;
    }
 
    void Update()
    {
        Vector2 newOffset = Vector2.zero;
 
        if (isScrollX)
        {
            float newOffSetX = material.mainTextureOffset.x + scrollspeed * Time.deltaTime;
            newOffset = new Vector2(newOffSetX, 0);
        }
        else
        {
            float newOffSetY = material.mainTextureOffset.y + scrollspeed * Time.deltaTime;
            newOffset = new Vector2(0, newOffSetY);
        }
 
        material.mainTextureOffset = newOffset;
    }
}
 

7. 스크롤 방향은 isScrollX 와 scrollspeed 음수 양수로 조절 하면 됩니다.


8. 동작이 안되는 경우

- 전혀 동작이 안된다... 이미지에 Meterial 설정이 제대로 반영되고 있는지 확인 바랍니다.

- 동작은 되는데 한번만 되고, 스크롤이 아래와 같은 현상이면 2번 설정을 확인해보시기 바랍니다.







2023년 12월 16일 토요일

Unity enum 을 string 획득하는 방법

서론

Unity에서 뭔가 nice하게 깔끔한 코드를 만들려는 압박이 있다보면, enum 형태를 생각하게 됩니다. 다른 언어에서 enum은 상수로만 사용이 가능한 부분도 있었는데 C#에서는 입력했던 String도 가져오는 방법이 준비가 되어있습니다.

본론

 Unity enum 은 C#에서 사용하는 열거형 형식을 가리키며, 이름이 지정된 상수 집합을 나타냅니다. 열거형은 enum 키워드를 사용하여 정의되며, 상수는 쉼표로 구분된 값의 목록으로 지정됩니다.

열거형의 모든 값들을 배열로 변환하기 위해 System.Enum.GetValues 메서드를 사용합니다. 이 메서드는 열거형의 모든 값들을 열거형의 기본 형식에 해당하는 배열로 반환합니다.

for 루프를 사용하여 배열의 각 요소에 접근합니다. 이때, 배열의 인덱스를 사용하거나 foreach 문을 사용할 수 있습니다.

각 요소를 string 으로 변환하기 위해 System.Enum.GetName 메서드를 사용합니다. 이 메서드는 열거형의 특정 값에 해당하는 이름을 반환합니다.

string 을 원하는 방식으로 사용하거나 저장합니다.

다음은 이 방법을 적용한 예제 코드입니다.


using UnityEngine;

public class EnumToString : MonoBehaviour
{
    public enum Colors
    {
        Red,
        Green,
        Blue
    }

    void Start()
    {
        var values = System.Enum.GetValues(typeof(Colors));

        for (int i = 0; i < values.Length; i++)
        {
            string name = System.Enum.GetName(typeof(Colors), values.GetValue(i));

            Debug.Log(name);
        }
    }
}


출력 결과

Red

Green

Blue


2023년 12월 13일 수요일

Unity making spreading bullet (방사 형태의 총알 발사 만들기)

2D 슈팅 게임을 만들어 보고 있는데 여러가지 형태의 총알 발사 형태가 필요해서 구현해 보았습니다.

Player 또는 적 -> 총알 발사하게 되는데 이것을 여기에서는 BulletGroupTest Object 생성하고 BulletGroupTest 는 여러발의 BulletTest(총알)을 생성 하도록 샘플을 만들어 보았습니다.


실제 동작 화면부터 보겠습니다.




가장 핵심의 되는 코드부터 설명하겠습니다.

BulletGroupTest -> BulletTest 를 생성 하는 코드

    public void Init()
    {
        // < 방사형
        if (bulletType == 0)
        {
            Vector3 targetPos = target.position;
            Vector3 dir = targetPos - transform.position;
            dir = dir.normalized;
            float baseAngle = 10f;
            float startAngle = -Mathf.Floor((bulletCount - 1) / 2) * (baseAngle) - baseAngle / 2 * (1 - bulletCount % 2);
            Vector3 curVec = Rotate_z(dir, startAngle);

            for (int i = 0; i < bulletCount; i++)
            {
                GameObject bullet = Instantiate(bulletPrefabPoolId, transform);
                bullet.transform.position = transform.position;
                bullet.transform.rotation = Quaternion.FromToRotation(Vector3.up, curVec);
                bullet.GetComponent<BulletTest>().Init(bulletDamage, bulletSpeed, bulletPiercing, curVec, bulletLifeTime);
                startAngle += baseAngle;
                curVec = Rotate_z(dir, startAngle);
            }
        }
        Destroy(gameObject,3f);
    }
    Vector3 Rotate_z(Vector3 dir, float angle)
    {
        // angle을 라디안으로 변환합니다.
        float radian = angle * Mathf.Deg2Rad;
        Vector3 ret = new Vector3(0, 0, 0);
        // 삼각함수를 이용하여 ret의 x, y, z 세 원소를 계산합니다.
        ret.x = dir.x * Mathf.Cos(radian) - dir.y * Mathf.Sin(radian); // x 방향으로 회전합니다.
        ret.y = dir.x * Mathf.Sin(radian) + dir.y * Mathf.Cos(radian); // y 방향으로 회전합니다.
        ret.z = dir.z; // z 방향은 변하지 않습니다.
        // ret를 결과로 반환합니다.
        return ret;
    }

이 부분이 하는 핵심 코드 입니다.

Bullet 를 생성하고 탄환을 일정 각도로 회전을 줍니다. 여기에서는 10 도 만큼 회전시키는 코드입니다. Rotate_z 함수는 Vector3 를 z축으로 특정 각도 만큼 회전시키는 함수입니다.


TestPlayer 코드 입니다.

해당 코드는 TestPlayer에 Componet로 추가 되어 있습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestPlayer : MonoBehaviour
{
    public GameObject bulletGroup;
    public int bulletCount = 3;
    public Transform target;
    public float speed = 100f;

    Rigidbody2D rb;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        float moveX;
        float moveY;

        moveX = Input.GetAxis("Horizontal");
        moveY = Input.GetAxis("Vertical");
        Vector3 moveVector = new Vector3(moveX, moveY, 0f);
        rb.velocity = moveVector.normalized * Time.fixedDeltaTime * speed;


        if (Input.GetButtonDown("Jump"))
        {
            GameObject bGroup = Instantiate(bulletGroup, transform);
            BulletGroupTest bGT = bGroup.GetComponent<BulletGroupTest>();
            bGT.bulletType = 0;
            bGT.bulletCount = bulletCount;
            bGT.bulletSpeed = 5;
            bGT.target = target;
            bGT.Init();
        }
    }
}

방향키로 움직이고 Jump키를 누르면 Bullet Group를 생성 시키는 함수 입니다.


앞서 BulletGroupTest의 핵심 코드를 설명하긴 했지만 전체 코드 입니다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletGroupTest : MonoBehaviour
{
    public int bulletType; // 0:총알 형태
    public int bulletCount;
    public float bulletDamage;
    public float bulletSpeed;
    public int bulletPiercing;
    public float bulletLifeTime;
    public Transform target;
    public GameObject bulletPrefabPoolId;
    public float bulletSize;

    void Awake()
    {

    }
    public void Init()
    {
        // < 방사형
        if (bulletType == 0)
        {
            Vector3 targetPos = target.position;
            Vector3 dir = targetPos - transform.position;
            dir = dir.normalized;
            float baseAngle = 10f;
            float startAngle = -Mathf.Floor((bulletCount - 1) / 2) * (baseAngle) - baseAngle / 2 * (1 - bulletCount % 2);
            Vector3 curVec = Rotate_z(dir, startAngle);

            for (int i = 0; i < bulletCount; i++)
            {
                GameObject bullet = Instantiate(bulletPrefabPoolId, transform);
                bullet.transform.position = transform.position;
                bullet.transform.rotation = Quaternion.FromToRotation(Vector3.up, curVec);
                bullet.GetComponent<BulletTest>().Init(bulletDamage, bulletSpeed, bulletPiercing, curVec, bulletLifeTime);
                startAngle += baseAngle;
                curVec = Rotate_z(dir, startAngle);
            }
        }
        Destroy(gameObject,3f);
    }
    Vector3 Rotate_z(Vector3 dir, float angle)
    {
        // angle을 라디안으로 변환합니다.
        float radian = angle * Mathf.Deg2Rad;
        Vector3 ret = new Vector3(0, 0, 0);
        // 삼각함수를 이용하여 ret의 x, y, z 세 원소를 계산합니다.
        ret.x = dir.x * Mathf.Cos(radian) - dir.y * Mathf.Sin(radian); // x 방향으로 회전합니다.
        ret.y = dir.x * Mathf.Sin(radian) + dir.y * Mathf.Cos(radian); // y 방향으로 회전합니다.
        ret.z = dir.z; // z 방향은 변하지 않습니다.
        // ret를 결과로 반환합니다.
        return ret;
    }
}



이번에는 날아가는 총알에 대한 코드입니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletTest : MonoBehaviour
{
    public float damage;
    public float speed;
    public int piercing;
    public float lifeTime;

    Rigidbody2D rigid;
    Animator animator;
    float accTime;

    void Awake()
    {
        rigid = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
    }
    public void Init(float damage, float speed, int piercing, Vector3 dir, float lifeTime, bool autoDie = false)
    {
        Debug.Log(speed);
        Debug.Log(dir);
        this.damage = damage;
        this.piercing = piercing;
        this.speed = speed;
        this.lifeTime = lifeTime;
        this.accTime = 0;
        rigid.velocity = dir * speed;
    }
    public void FixedUpdate()
    {
        accTime += Time.fixedDeltaTime;
        if (accTime > lifeTime)
        {
            Destroy(gameObject);
        }
    }

}

BulletTest 는 총알 이미지로 Prefab화 시켰습니다.




2023년 11월 26일 일요일

Unity destructible tilemap (유니티에서 깨지는 타일맵 효과 만들기)

일반적인 2D 플래포머 게임에서는 잘 사용하지는 않지만 타일로 구현된 맵이 깨지는 형태의 게임이 있습니다. 대표적으로 테라리아 같은 게임입니다.

타일로 제작된 Object가 깨질때 효과를 여기에서 다뤄 보겠습니다.

소스 코드는 아래 유튜브 영상에서 가져온 뒤 변형하였습니다.

https://youtu.be/jbejkt-Ow-8


1. 완성 영상


처음 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를 여러개 두고 파괴가 안되는 처리도 가능합니다.

public class MouseInput : MonoBehaviour
{
    Vector3 mousePosition;
    public GameObject impactPointPrefab;
    public Tilemap destructibleTileMap;

    // Update is called once per frame
    void Update()
    {
        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생성 후 설정을 하도록 구현 하였습니다.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class ImpactPoint : MonoBehaviour
{
    private int updateTimer = 0;
    public int maxTime;

    public Vector2Int defaultRadius;
    public Tilemap destructibleTileMap;
    public GameObject tileSplitterPrefab;

    private List<Vector3Int> tilesToBreak;

    // Start is called before the first frame update
    void Start()
    {
        tilesToBreak = GetAffectedTiles(transform.position, defaultRadius);
    }

    private void FixedUpdate()
    {
        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;
            }
        }
        else Destroy(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장의 이미지를 그렸습니다.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TileSplitter : 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 update
    void Start()
    {
        BreakSpriteToMasks();
    }

    private void BreakSpriteToMasks()
    {
        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);
    }

    private void UpdateSpriteObject(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;
        }
    }
    private void UpdateShapeToSprite(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 컴포넌트도 추가해줍니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BrokenSpriteFadeOutHandler : MonoBehaviour
{
    [SerializeField] private float fadeSpeed;
    [SerializeField] private int destroyDistance;
    [SerializeField] private int forceFadeTimer;

    private Rigidbody2D rb;
    private bool startFade;
    private int forceFade;
    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        transform.Rotate(0,0,90*Random.Range(0,4));
    }
    private void LateUpdate()
    {
        float distanceToCamera = Vector2.Distance(transform.position, Camera.main.transform.position);
        if (distanceToCamera > destroyDistance) Destroy(gameObject);
        else if (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 frame
    void Update()
    {
        
    }
}

코드 상으로는 별내용은 없습니다. 일정시간뒤 Fadeout 되도록 설정합니다.

4. 전체 소스