이 부분을 완전히 정복해보자!


목차

  1. Has Exit Time
  2. Exit Time
  3. Fixed Duration
  4. Transition Duration
  5. Transition Offset
  6. 현재 State를 받는 법

Has Exit Time

  • Exit Time의 사용 여부를 결정한다.

Exit Time

  • Exit Time이란 특정 State에 진입했을 시, Transition 조건을 충족했더라도 다음 State로 넘어가지 않은 채로 버티는 시간을 말한다. 단위는 (Animation Clip의 재생 시간에 기반한) normalized이다.
  • 주로 격투 게임에서 State Delay를 구현하는 데 사용된다. (예 : 땅에 착지하는 모션이 모두 재생된 다음에야 파동권을 쏠 수 있도록 만들기 위해 사용됨)

Fixed Duration

  • Transition Duration의 단위를 결정한다; "실제 초(fixed)"로 할 것인지, "normalized(%)"로 할 것인지?

Transition Duration

  • 현재 State와 다음 State를 얼마만큼 Blend하여 Transition할 것인지 결정한다. 범위는 0 ~ 1까지이다. 이 값이 크면 클수록 자연스럽다. 그러나 너무 크면 Input과 Action의 딜레이가 크다는 느낌을 받을 수도 있으니, 0.2 ~ 0.25 정도로만 정하자.
  • 참고로 Sprite Sheet Animation이라면 무조건 0으로 설정해야 한다. 자연스러운 Blend가 불가능하다.

Transition Offset

  • 애니메이션을 어느 시점에서부터 재생할지 결정하는 옵션이다.
  • 대표적인 사용처로는 Idle -> Walk Transition에서 Walk State의 Transition Offset을 25%로 설정하는 것이다. 그러면 발이 순간이동하는 것처럼 보이는 문제를 해결할 수 있다.

현재 State를 받는 법

  • AnimatorStateInfo를 통해 받을 수 있다.
  • 사용 예 : Update() 내부에 AnimatorStateInfo stateinfo_base = anim.GetCurrentAnimatorStateInfo(0)를 넣는다.
  • 참고로 GetCurrentAnimatorStateInfo(int)에서 int는 레이어를 넣는 곳이다. 3D Rig Humanoid Animation에서 자주 사용되는 개념으로 0이면 Base, 1이면 Upper Body이다.

읽어주셔서 감사합니다!

GitHub Desktop 최고!


Unity 전용 .gitignore

  • GitHub에서 Repository를 새로 만들 때 .gitignore 항목에서 다운로드 받을 수도 있다. 

.gitignore
0.00MB


Unity Editor 설정 변경

  • Edit >> Project Settings >> Version Control >> Mode를 Visible Meta Files로 변경
  • git push & pull을 할 때마다 mesh collider의 위치가 엇갈리는 것을 방지해준다.

초기화 (자신이 팀장인 경우)

  • 작업하는 directory는 Unity > Project > Currently Working Project인 것으로 간주한다.
  1. Unity 전용 gitignore을 Currently Working Project에 넣어준다.
  2. git config --global init.defaultBranch main를 친다. 이후에 git init을 할 때마다 기본 branch가 main이 된다. 
  3. git init
  4. git remote add origin url (remote에 있는 빈 리포지토리의 url)
  5. git add .
  6. git commit -m "first commit"
  7. git push

초기화가 올바르게 진행됐는지 확인

  1. git remote add origin 이후에 git remote -v
  2. git add . 이전에 git status

이후 작업할 때

  1. git checkout main (현재 브랜치를 main으로 이동)
  2. git pull (최신 상태 반영)
  3. git branch 브랜치이름 (작업용 브랜치 생성)
  4. git checkout 브랜치이름
  5. 작업하기... (git add ., git commit -m "asdf", git push)
  6. 이후 GitHub Repository에 가보면 PR이 있을 것. 
  7. git conflict를 해결한 뒤, merge. 또한 밑에 delete branch 버튼이 있을 것. 이것도 눌러준다.
  8. git branch -d 브랜치이름 (작업한 브랜치를 로컬에서 지운다)

차선책

  • 엔진을 다룰 수 있는 기획자 & 아트 직군과 협업하는 경우, git보다는 GitHub Desktop을 사용하는 것이 더 효율적일 수도 있다.
  • GitHub Desktop은 GUI로, commit, push, pull, branch 교체가 자유롭고 편하다.
  • 이렇게 작업하는 경우, 주로 리드 프로그래머가 베이스 프로젝트를 main에 push 한 뒤, stage와 같은 협업용 branch를 따로 판다.
  • 협업용 branch에는 자유롭게 push를 할 수 있으나, conflict를 방지하기 위해 push 여부를 최대한 자주 디스코드에 알려줘야 한다.
  • conflict가 난 경우 VS Code를 통해 빠르게 고칠 수 있다; HEAD 쪽을 잘 보면 위에 작은 글씨로 incoming과 같은 옵션을 선택할 수 있다.

읽어주셔서 감사합니다!

출시까지 단 한 걸음...!


앱에 관한 설정은...

보통 Edit > Project Settings > Player에서 많이 한다. 후술할 항목들도 이곳에서 설정값을 만진다는 가정 하에서만 유효하다. 처음 Player 창에 들어가면 Company Name부터 시작해서 Icon, Resolution and Presentation, Splash Image, Other Settings가 보일 것이다. 


기본 설정

  • Company Name : 제작자의 이름을 적는 곳
  • Product Name : 게임의 이름을 적는 곳
  • Version : 앱스토어에 표시될 버전을 적는 곳. 그러나 실제로 버전이 "달라졌다는" 사실은 다른 항목으로 판정한다.
  • Default Icon : 앱 아이콘

Resolution and Presentation

  • Orientation > Default Orientation에서 Auto Rotation으로.
  • Orientation > Allowed Orientation for Auto Rotation에서, 가로 게임이면 Landscape Right & Landscape Left만 체크. 세로 게임이면 Portrait & Portrait Upside Down만 체크.

Splash Image

  • Logos 리스트에 자신이 원하는 Logo를 추가할 수 있다. 
  • Preview를 통해 취향껏 커스텀하자.
  • Unity 6.0 버전에서도 Unity Logo를 지울 수 있다.

Other Settings

  • Identification > Override Default Package Name 체크 해제. (이유 : Unity가 Company Name과 Product Name을 합쳐 자동 생성한 Package Name은 바꾸지 않는 것이 좋다. 이게 다르면 플랫폼이 아예 다른 앱으로 인식하고, 업데이트 대신 "새로 설치"를 종용하기 때문)
  • Identification > Bundel Version Code는 앱을 업데이트 할 때마다 1씩 늘려줘야 한다. (이유 : 플랫폼에서, 맨 위에 있는 Version이 아닌 Bundle Version Code를 통해 앱의 업데이트 여부를 확인하기 때문)
  • Identification > Minimum API Level은 자주 바뀌는 편이다. 자신이 앱을 출시할 플랫폼의 정책에 맞춰 설정하자.
  • Configuration > Scripting Backend를 목적에 맞춰 설정하자. 출시용이라면 IL2CPP로, 빌드 테스트용이라면 Mono로.
  • Configuration > Target Architectures에서 ARM64에 체크. Google Play Store에 출시하려면 필수다.

Publishing Settings

  • Project Keystore > Custom Keystore를 체크. 이후 Select... Browse...를 통해 잘 선택한 뒤 (Keystore) Password를 맞게 치면 Project Key > Alias를 선택할 수 있게 된다. 마찬가지로 Password를 맞게 쳐주면 출시 준비 완료!

마지막으로...

  • Edit > Project Settings > Player가 아닌, File > Build Profiles > Andriod로 가서 Platform Settings 밑에 있는 Build App Bundle (Google Play)를 체크해야 한다. 

읽어주셔서 감사합니다!

Player Input

using UnityEngine;

public class PlayerInput : MonoBehaviour
{
    public float move_x { get; private set; }
    public float move_z { get; private set; }
    public bool jump { get; private set; }

    private void Awake()
    {
        InitFields();
    }

    private void InitFields()
    {
        move_x = 0;
        move_z = 0;
        jump = false;
    }

    private void Update()
    {
        // if (GameManager.game_over) { InitFields(); return; }

        move_x = Input.GetAxisRaw("Horizontal");
        move_z = Input.GetAxisRaw("Vertical");
        jump = Input.GetKeyDown(KeyCode.Space);
    }
}

Player Action

using UnityEngine;

public class PlayerAction : MonoBehaviour
{
    private PlayerInput pi;
    private Rigidbody rb;

    private float move_speed;
    private float jump_force;
    private float vel_damp;

    [SerializeField] private Transform cam_transform;

    private void Awake()
    {
        GetReferences();
        InitFields();
    }

    private void GetReferences()
    {
        pi = GetComponent<PlayerInput>();
        rb = GetComponent<Rigidbody>();
    }

    private void InitFields()
    {
        move_speed = 100f;
        jump_force = 500f;
        vel_damp = 0.75f;
    }

    private void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    private void FixedUpdate()
    {
        Move();
        // if (pi.jump) Jump();
    }

    private void Update()
    {
        if (pi.jump && IsGrounded()) Jump();
        transform.rotation = Quaternion.Euler(transform.eulerAngles.x, cam_transform.eulerAngles.y, transform.eulerAngles.x);
    }

    private bool IsGrounded()
    {
        Collider[] hits = Physics.OverlapBox(new Vector3(transform.position.x, transform.position.y - 1f, transform.position.z), new Vector3(0.5f, 0.25f, 0.5f), Quaternion.identity, LayerMask.GetMask("Ground"));
        if (hits.Length > 0) return true;
        return false;
    }

    private void Move()
    {
        Vector3 move_dir = (transform.right * pi.move_x + transform.forward * pi.move_z).normalized;
        rb.linearVelocity += move_dir * move_speed * Time.fixedDeltaTime;
        rb.linearVelocity = new Vector3(rb.linearVelocity.x * vel_damp, rb.linearVelocity.y, rb.linearVelocity.z * vel_damp);
    }

    private void Jump()
    {
        rb.linearVelocity = new Vector3(rb.linearVelocity.x, 0, rb.linearVelocity.z);
        rb.AddForce(Vector3.up * jump_force);
    }
}

Main Camera

  • Cinemachine Brain

Virtual Camera

  • Cinemachine Camera
    Tracking Target : Player
    Position Control : Orbital Follow
    Rotation Control : Rotation Composer
  • Cinemachine Orbital Follow
    Position Damping : X, Y, Z 전부 0 (안 그러면 카메라가 플레이어를 제대로 따라가지 못하고, 중간에 뚝뚝 끊김!)
    Orbit Style : Sphere
    Radius : 10
  • Cinemachine Rotation Composer
  • Cinemachine FreeLook Modifier
  • Cinemachine Input Axis Controller

읽어주셔서 감사합니다!

using UnityEngine;
using System;
using GoogleMobileAds;
using GoogleMobileAds.Api;
using UnityEngine.SceneManagement;

public class AdManager : MonoBehaviour
{
    public static GameObject ad_manager;
    private BannerView banner_view;

    private void Awake()
    {
        if (ad_manager == null)
        {
            ad_manager = gameObject;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    public void Start()
    {
        MobileAds.Initialize((InitializationStatus initStatus) => { }); // Initialize the Google Mobile Ads SDK.
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    private void RequestBanner()
    {
        #if UNITY_ANDROID
            string _adUnitId = "ca-app-pub-3940256099942544/6300978111";
        #elif UNITY_IPHONE
            string _adUnitId = "ca-app-pub-3940256099942544/2934735716";
        #else
            string _adUnitId = "unused";
        #endif

        //Clean up banner ad before creating a new one.
        if (banner_view != null)
        {
            banner_view.Destroy();
        }

        // Create a adaptively-sized banner at top of the screen
        AdSize adaptiveSize = AdSize.GetCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(AdSize.FullWidth);
        banner_view = new BannerView(_adUnitId, adaptiveSize, AdPosition.Bottom);

        // create our request used to load the ad.
        var adRequest = new AdRequest();

        // send the request to load the ad.
        banner_view.LoadAd(adRequest); //Debug.Log("Loading banner ad.");
    }

    private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        if (scene.name == "AD Scene")
        {
            RequestBanner();
        }
        else
        {
            if (banner_view != null)
            {
                banner_view.Destroy();
            }
        }
    }
}
using UnityEngine;
using System.IO;

public class DataManager : MonoBehaviour
{
    public static GameObject data_manager;
    public Data data;

    private void Awake()
    {
        if (data_manager == null)
        {
            data_manager = gameObject;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Start()
    {
        data = new Data();
        LoadFromJson();
    }

    private void OnApplicationQuit()
    {
        SaveToJson();
    }

    public void SaveToJson()
    {
        string filePath = Application.persistentDataPath + "/InGameData.json";
        File.WriteAllText(filePath, JsonUtility.ToJson(data));
    }

    public void LoadFromJson()
    {
        string filePath = Application.persistentDataPath + "/InGameData.json";
        if (File.Exists(filePath) == false)
        {
            ResetData();
            return;
        }
        data = JsonUtility.FromJson<Data>(File.ReadAllText(filePath));
    }

    public void ResetData()
    {
        for (int i = 0; i < 45; i++) data.cleared_levels[i] = false;
    }

    public void UpdateClearedLevel(int cleared_level_index)
    {
        data.cleared_levels[cleared_level_index] = true;
    }
}

[System.Serializable]
public class Data
{
    public bool[] cleared_levels = new bool[45];
}

 

Preview

https://youtu.be/k9zzi_xrEK4

??? : 탱커가 트롤인데요?

 


필요한 Script

  • Player
  • Watcher

Player

필요한 Component;

  • Sprite Renderer
  • Rigidbody 2D : Body Type(Dynamic), Constrains(Freeze Rotation Z)
  • Player(Script) : 하단 참조.
using UnityEngine;

public class Player : MonoBehaviour
{
    private Rigidbody2D rb;

    private float speed;
    private float vel_damp;
    private Vector2 acc_direction;

    private void Awake()
    {
        GetReferences();
        InitFields();
    }

    private void GetReferences()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    private void InitFields()
    {
        speed = 100;
        vel_damp = 0.75f;
        acc_direction = Vector2.zero;
    }

    private void FixedUpdate()
    {
        AdjustMovement();
    }

    private void AdjustMovement()
    {
        rb.linearVelocity += acc_direction * speed * Time.fixedDeltaTime;
        rb.linearVelocity *= vel_damp;
    }

    private void Update()
    {
        Move();
    }

    private void Move()
    {
        acc_direction = Vector2.zero;

        if (Input.GetKey(KeyCode.RightArrow)) acc_direction += Vector2.right;
        if (Input.GetKey(KeyCode.LeftArrow))  acc_direction += Vector2.left;
        if (Input.GetKey(KeyCode.UpArrow))    acc_direction += Vector2.up;
        if (Input.GetKey(KeyCode.DownArrow))  acc_direction += Vector2.down;

        acc_direction = acc_direction.normalized;
    }
}

 


Watcher

필요한 Component;

  • Sprite Renderer
  • Watcher(Script) : 하단 참조.
using UnityEngine;

public class Watcher : MonoBehaviour
{
    private Player player;

    private float fov;
    private float detecting_distance;
    private float rotation_speed;

    private void Awake()
    {
        GetReferences();
        InitFields();
    }

    private void GetReferences()
    {
        player = FindFirstObjectByType<Player>();
    }

    private void InitFields()
    {
        fov = 90f;
        detecting_distance = 3.5f;
        rotation_speed = 50f;
    }

    private void FixedUpdate()
    {
        TurnHead();
    }

    private void TurnHead()
    {
        // False Conditions
        if (Vector2.Distance(transform.position, player.transform.position) > detecting_distance) return;

        Vector2 current_direction = transform.right;
        Vector2 target_direction = (player.transform.position - transform.position).normalized;
        float angle_difference = Vector2.SignedAngle(current_direction, target_direction);
        if (Mathf.Abs(angle_difference) > fov / 2f) return;
        if (Mathf.Abs(angle_difference) <= rotation_speed * Time.fixedDeltaTime) return;

        // If True
        transform.Rotate(0, 0, Mathf.Sign(angle_difference) * rotation_speed * Time.fixedDeltaTime);
    }
}

 


읽어주셔서 감사합니다!

궁금한 점이 있다면 댓글로 문의주세요!

Scriptable Object란?

데이터를 담는 Unity 전용 Asset이다. 주로 Prefab을 쉽게 관리하기 위해 쓰인다. 그러나 이렇게만 보면 어떻게 사용해야 하는지 도무지 감이 잡히지 않을 것이다. 필자도 처음에는 그랬기 때문에, 예시를 들면서 진행하겠다.


예시 상황

여러 가지 Projectile을 만들고 싶다. 그래서 Projectile 클래스를 만들었다.

 

보통의 경우, 이 시점에서 두 가지 방법을 사용하여 다양한 Projectile을 만들 수 있다.

  1. 이 Projectile 클래스를 상속하여 Wooden Arrow와 Iron Arrow 클래스를 만든다. 그렇게 상속을 통해 만들어진 클래스들을 각각의 GameObject에 붙인 뒤, Prefab으로 만든다.
  2. 또는? Projectile 클래스에 damage, speed, gravity와 같은 필드 변수를 public 또는 [SerializedField] & private으로 만든다. 그 후, Inspector 창에서 수치를 직접 조절한 뒤, Prefab으로 만든다. 

보통의 경우에는 위의 두 가지 방법이 매우 효과적인 해결책이 될 수 있다. 그러나 Scriptable Object를 알고 있는 사람의 관점에서는 그렇지 않다. 왜냐하면, Scriptable Object는 "변하는 데이터만 담아두는" Asset이기 때문이다.


Scriptable Object로 구현한 Projectile을 위한 데이터

using UnityEngine;

public enum ElementalType
{
    None,
    Fire,
    Ice,
    Spark
}

[CreateAssetMenu(fileName = "New Projectile Data", menuName = "Projectile Data")]
public class ProjectileData : ScriptableObject
{
    public Sprite texture;
    public float damage;
    public Vector2 direction;
    public float speed;
    public float gravity;
    public ElementalType elemental_type; 
}

 

위와 같은 코드를 쓴 뒤, 아래 사진과 같이 조작하면 "Projectile을 위한 Data Asset(=Projectile Data)"을 만들 수 있다;

Projectile Data 생성!

 

그 후, 각각의 데이터를 아래 사진과 같이 잘 적어내려가면 된다.

참고로 Enum을 쓰면 elemental type을 매우 편하게 설정할 수 있다!


Projectile 클래스

using UnityEngine;

public class Projectile : MonoBehaviour
{
    private Rigidbody2D rb;
    private SpriteRenderer sr;

    private float damage;
    private Vector2 direction;
    private float speed;
    private float gravity;

    [SerializeField] private ProjectileData projectile_data;

    private void Awake()
    {
        GetComponents();
        GetData();
    }

    private void Start()
    {
        rb.linearVelocity = direction.normalized * speed;
        rb.gravityScale = gravity;
    }

    private void FixedUpdate()
    {
        transform.right = rb.linearVelocity.normalized;
    }

    private void GetComponents()
    {
        rb = GetComponent<Rigidbody2D>();
        sr = GetComponent<SpriteRenderer>();
    }

    private void GetData()
    {
        sr.sprite = projectile_data.texture;
        damage = projectile_data.damage;
        direction = projectile_data.direction;
        speed = projectile_data.speed;
        gravity = projectile_data.gravity;
    }

    private void OnBecameInvisible()
    {
        Destroy(gameObject);
    }
}

 

빈 GameObject를 생성한 뒤, Sprite Renderer, Projectile (Script), Rigidbody 2D 컴포넌트를 달아주자. 이름은 Projectile로 해주자. 그 후, 이렇게 만든 Projectile Script에 Projectile Data를 Inspector 창을 통해 넣어주면 된다.


간단한 Preview

Trail은 자연스럽게 보이도록 넣었다!

 

이제 이렇게 만든 Projectile을 Prefab으로 만든 뒤에, Instantiator를 만들어주면 "매우 쉽고 편하게" 다양한 종류의 Projectile을 만들 수 있다!


읽어주셔서 감사합니다!

궁금한 점이 있다면 댓글로 문의주세요!

생각보다...

탑뷰로 진행되는 2D 게임에서, 그림자를 어떻게 구현해야 할지에 대한 가이드가 잘 없다. (그리고 애초에 이런 걸 쉽게 구현할 수 있는 방법이 Unity 6 이전까지는 존재하지가 않았다) 그러나 Unity 6으로 업그레이드 되면서, 2D 관련 Shadow 컴포넌트가 많은 업그레이드를 받았고, 그랬기에 매우 쉽게 구현할 수 있게 됐다! 그래서 오늘은 이 "쉬운 구현법"을 소개해보고자 한다.

 

이 가이드가 끝날 무렵에는 이런 것들을 마음대로 갖고 놀 수 있을 것이다;

벽 바깥쪽으로는 광원이 새어나가지 않는다!


Background Tilemap

먼저, 2D 게임에는 3D 게임의 Directional Light와 같은 개념이 없어서, 배경이 될 Object가 없으면, 자기 자신의 위에 비추는 Light가 아닌 경우, 아무런 Light 효과도 볼 수가 없다는 사실을 알아야 한다. 만약 Tilemap에 대해 잘 알지 못한다면, 다음 글을 보면 도움이 많이 될 것이다!

2024.06.02 - [코딩테스트/알고리즘] - [알고리즘] Flood Fill 알고리즘 (C)

 

[알고리즘] Flood Fill 알고리즘 (C)

서론Unity로 2D 게임을 작업하다 보면, 상당히 불편한 점이 많다. 그럴 땐 3D로 넘어가도록 하자 특히 Lighting 부분에서 그 단점이 도드라지는데, 그 이유는 Spot Light 2D를 제대로 사용할 수 없기 때문

quickclid.tistory.com

 

적당히 Background Tilemap을 깔아줬다면 다음 스텝으로 넘어가자.


Brick Prefab

  1. Hierarchy 창에서 우클릭을 한 뒤, 2D Object > Sprites > Square을 선택하여 Brick GameObject를 하나 만들어준다.
  2. 그 후, Shadow Caster 2D 컴포넌트를 추가한다.
  3. Shadow Caster 2D 컴포넌트에서, Casting Option을 "Cast And Self Shadow"로 한다. 이 옵션은 자기 자신(Sprite 기준으로)의 위쪽에도 Shadow를 Cast하게 만든다.
  4. Shadow Caster 2D 컴포넌트에서, Alpha Cutoff를 0으로 한다. (이걸 안 하면, "Cast And Self Shadow"를 사용했을 때 그림자 사이에 실같은 틈이 생긴다)

이제 이렇게 만든 GameObject를 Assets 창으로 드래그하여 Prefab로 만들면 된다.


Brick Tilemap

위에서 만들었던 Background Tilemap은 그대로 두고, 새로운 "Brick" Tilemap을 하나 만들자. 이 Tilemap에는 처음 만들었을 시에 기본적으로 추가되어있는 "Tilemap", "Tilemap Renderer" 컴포넌트 말고도, "Composite Shadow Caster 2D" 컴포넌트도 추가해야 한다.

 

이후 Tilemap Palette를 열고, 시선을 Tile Palette의 (적당한) 왼쪽 아래로 옮기면 "Default Brush"가 보일 것이다. 이건 어떤 브러쉬를 사용할지 결정하는 옵션이다. 이 브러쉬를 GameObject Brush로 바꾼 뒤, Cells를 클릭하면 "Element 0"이 보일 것이다.

 

이 Element 0을 클릭하면 여러 가지 것들이 보일텐데, 지금은 Game Object에 우리가 위에서 만든 Brick Prefab을 넣어주기만 하면 된다.

 

Prefab을 넣은 뒤, Tile Palette의 (적당한) 오른쪽 위로 시선을 옮기면 "연필/격자/구"가 보일 텐데, 여기서 "연필 모양"이 "Toggles Tile Palette Edit"이다. 이 모드를 활성화하면 Tilemap을 편집할 수 있다. 이걸 켜준 뒤 Palette의 아무 곳이나 클릭하면 Prefab이 Palette 위에 저장된다. (Toggles Tile Palette Edit 모드를 해제한 뒤) 이제 이걸 마음대로 Scene에 칠하면 된다.


Player

  1. Hierarchy 창에서 우클릭을 한 뒤, 2D Object > Sprites > Square을 선택하여 Player GameObject를 하나 만들어준다.
  2. Light 2D 컴포넌트를 추가해준다. Light Type은 "Spot"으로, Radius는 5 정도로 해준다. 
  3. 만약 Shadows 옵션이 활성화(체크 표시) 되어있지 않은 경우, 활성화한다. 

Shadows에 대한 부가 설명 (필요하지 않다면 넘어가도 된다!):

더보기

기본적으로 Shadows는 Light 2D 컴포넌트에서 설정할 수 있는 옵션이므로, "이 객체가 발산하는 빛"에 대한 그림자가 어떻게 드리우도록 만들지 결정하는 것이다.

 

Strength는 그림자의 불투명도를 조절한다. 1이라면, 그림자가 드리운 영역이 완전히 검게 표시된다. 당연히 다른 오브젝트도 안 보인다. 0이라면, 그림자가 아예 안 보인다. 

 

Softness는 "그림자-모서리"의 표현 방식을 결정한다. 이 값이 클수록 그림자의 모서리가 좀 더 부드럽게 보인다.

 

Falloff Strength는 Softness와 연계되는 옵션이다. 이 값이 작을수록 그림자의 모서리가 보다 퍼지는 느낌이 된다.

 

비록 Player GameObject는 그림자가 잘 적용되는지 테스트하기 위해 만든 것이지만, 움직이게 만들고 싶다면 다음과 같은 코드를 적용해주면 된다!

 

코드

작업중...

 

약간의 수정을 거친 뒤 더 좋아진 코드로 돌아오겠다!


Torch

Player GameObject와 완전히 똑같다. 다만 차이점이라면 움직일 수 없다는 정도?

 

조금 더 재미있게 사용해보고 싶다면, Torch가 지닌 Light 2D 컴포넌트에서, Color을 바꿔보면 좋다. 예를 들어, Torch를 2개 만든 뒤, 하나는 Color를 빨간색으로, 다른 하나는 파란색으로 한 뒤 Raduis를 좀 크게 해서 겹치는 범위를 만들어보면, 보라색이 되는 것을 확인할 수 있다. (분명 빛인데, 색처럼 섞인다!)

예쁘다...


읽어주셔서 감사합니다!

궁금한 사항이 있다면 댓글로 언제든지 질문 주세요!

Stack 영역

  • LIFO(Last in, First out) 방식으로 데이터가 저장된다.
  • 할당과 해제가 자동으로 이루어진다.
  • 값 타입을 저장한다.
  • 수명이 짧은 편이다. (함수가 return되는 즉시, 모든 지역변수가 pop된다!)
  • 실행할 수 없다. (누군가가 Stack에 코드를 써넣을 수도 있다. 이게 해킹 수단이 될 수도 있다!)
  • 높은 주소 -> 낮은 주소 순으로 할당된다.

Heap 영역

  • 프로그래머가 직접 할당과 해제를 해야 한다.
  • 참조 타입을 저장한다.
  • 실행할 수 없다. (마찬가지로 Heap에도 코드를 주입할 수 있기 때문)
  • 낮은 주소 -> 높은 주소 순으로 할당된다.

여기서 값 타입이란? 변수에 값을 저장하는 타입을 뜻한다. 이는 두 가지 특징을 지니는데;

  1. 다른 변수에 할당하거나 함수로 전달하는 경우, 값이 복사된다.
  2. 복사되기 때문에 원본 값이 변경되지 않는다.

여기서 참조 타입이란? 주소를 저장하는 타입을 뜻한다. 이는 두 가지 특징을 지니는데;

  1. 다른 변수에 할당하거나 함수로 전달하는 경우, 같은 객체를 참조한다.
  2. 한쪽에서 데이터를 변경하면, 모두가 영향을 받는다.

 

+ Recent posts