본문 바로가기

Unity/로직 설계

[Unity] 책 넘기는 효과 구현하기 (Page Curl) - 2

한 페이지를 넘겼다!

 

지난 포스팅에선 위 부분까지 구현했었다.

이제 음영을 넣어 자연스레 만들고, 페이지를 늘려 책 한 권을 완성시켜보자.


페이지 늘리기

원래 음영을 먼저 하는 게 자연스러운데, 까먹고 늘리는 걸 먼저 해버렸다. (이러면 나중에 음영을 일일이 추가해줘야 한다.)

 

먼저 프리팹화하고, 하이어라키 창에 여럿으로 늘렸다.

 

 

그리고 Blocker는 Book의 바깥쪽으로 빼줬는데, 이후 코드에서 Child를 가져올 때 섞이지 않게 하기 위함이다. 개념적으로도 이게 맞고.

 

 

그리고 동시에, Page 내 Back Pivot이 제어하던 이동 함수(MoveCorner)를 Book이 전부 관리하게 바꿨다. 굳이 왜 그랬냐면 MoveCorner 스크립트를 또 배열에 담는 게 낭비 같아서...

 

 

 

그렇게 코드는 아래처럼 변경되었다.

 

using DG.Tweening;
using System;
using UnityEngine;

public class PageCurl : MonoBehaviour
{
    // 전체 페이지 
    private Transform[] pages;

    // 구성 요소 (앞면, 뒷면, 마스크)
    private Transform[] frontPage;
    private Transform[] backPage;
    private Transform[] mask;

    // 넘김 효과가 실행 중인가?
    private bool isCurling = false;
    // 넘길 페이지 번호. 한 장 넘긴 후 증가시켜 다음 장을 넘긴다.
    [SerializeField] private int pageNumber = 0;

    // 페이지의 꼭짓점과 책의 꼭짓점
    private Vector2 point;
    private Vector3 corner = new Vector3(300f, -225f, 0f);

    // BackPage의 시작 위치
    private Vector3 firstPosition;

    public void Awake()
    {
        // 배열 초기화
        pages = new Transform[transform.childCount];
        frontPage = new Transform[transform.childCount];
        backPage = new Transform[transform.childCount];
        mask = new Transform[transform.childCount];
        
        // 배열에 자식들을 불러온다.
        for(int i = 0; i < transform.childCount; ++i)
        {
            // 맨 아래의 자식부터 불러온다.
            pages[i] = transform.GetChild((transform.childCount - 1) - i);

            // 나머진 pages를 기준으로 불러오니 i가 들어갔다.
            frontPage[i] = pages[i].GetChild(0).GetChild(0).GetChild(0);
            backPage[i] = pages[i].GetChild(0).GetChild(0).GetChild(1);
            mask[i] = pages[i].GetChild(0);
        }
        
        // 코너를 책 위치 기준으로 해야 하니 책 위치를 더해준다.
        corner += transform.position;
        // 시작 위치를 미리 받아둔다.
        firstPosition = backPage[0].transform.position;

        Debug.Log(corner);
        Debug.Log(backPage[0].transform.position);
    }

    public void Update()
    {
        // Curl 효과가 동작하는 동안
        if (isCurling)
        {
            // 넘김 효과를 실행한다.
            CurlPage(pageNumber);
        }
    }
    
    // 페이지를 넘긴다.
    public void FlipPage()
    {
        if (isCurling || pageNumber >= transform.childCount)
        {
            return;
        }

        isCurling = true;

        MoveBackPage(pageNumber);
    }

    // 페이지를 움직인다.
    private void MoveBackPage(int i)
    {
        // 틀어질 경우를 대비해, 시작 위치로 이동시킨다.
        backPage[i].transform.position = firstPosition;

        // 위치를 포물선으로 이동시킨다. (개선 필요)
        DOTween.Sequence()
            .Append(backPage[i].transform.DOMoveX(firstPosition.x - 300f, 1f))
            .Join(backPage[i].transform.DOMoveY(firstPosition.y + 150f, 1f))
            .Append(backPage[i].transform.DOMoveX(firstPosition.x - 600f, 1f))
            .Join(backPage[i].transform.DOMoveY(firstPosition.y, 1f)).SetEase(Ease.OutCubic)
            // 끝나면 Curling 종료, 페이지 번호 증가, 다음 장을 제일 위로 올리기
            .OnComplete(() => { 
                isCurling = false;
                pageNumber++;
                pages[pageNumber].SetAsLastSibling();
            });
    }

    // 페이지 넘김 효과를 실행한다.
    private void CurlPage(int i)
    {
        // 책 오른쪽 페이지의 우측 하단 꼭지점.
        point = backPage[i].transform.position;

        // x, y 계산
        float x = corner.x - point.x;
        float y = point.y - corner.y;

        // 세타(각도) 계산, 단위는 Degree(도)
        // x == 0인 경우를 처리하기 위해, Atan이 아닌 Atan2를 쓴다.
        float theta = Mathf.Atan2(y, x) * Mathf.Rad2Deg;

        // BackPage, FrontPage가 Mask에 영향받아 움직이지 않게, 미리 위치를 캐싱해둔다.
        Vector3 firstFrontPagePosition = frontPage[i].position;
        Vector3 firstBackPagePosition = backPage[i].position;

        // Mask의 이동할 거리 계산 및 이동
        float maskX = (Vector2.Distance(point, corner) / 2) / Mathf.Cos(theta * Mathf.Deg2Rad);
        mask[i].position = corner - new Vector3(maskX, 0f, 0f);
        // Mask 회전
        mask[i].rotation = Quaternion.Euler(0f, 0f, -theta);

        // BackPage, FrontPage 위치를 원래대로 바꾼다.
        backPage[i].position = firstBackPagePosition;
        // BackPage의 회전은 계산한 결과대로 변경
        backPage[i].rotation = Quaternion.Euler(0f, 0f, -2 * theta);

        // FrontPage는 위치, 회전 고정
        frontPage[i].position = firstFrontPagePosition;
        frontPage[i].rotation = Quaternion.Euler(0f, 0f, 0f);
    }
}

 

isCurling이 추가된 건 넘어가는 중에 또 넘기지 않게 하기 위함인데, 이는 개념적으로 허용될 일이지만 CurlPage 함수 두 개를 동시에 돌려야 하기에 막아뒀다. 현재 개발 중인 게임엔 투 머치기도 하고.

 

배열을 전부 Reverse한 건 맨 아래의 UI가 맨 앞에 보이기 때문으로, 즉 첫 장은 맨 아래에 있다.

Reverse로 미리 바꾸는 것보단 반복문을 뒤쪽부터 돌리는 게 비용이 적지만, 큰 차이도 아니고 개념적으로 이해하기 편해서 이렇게 구현해뒀다. 500장 이상 넘어가면 구조 변경을 고려해볼 법하다. 

 

실제로 호출되는 건 FlipPage 함수가 전부다. 원하는 시점에 저 함수만 실행시키면 페이지가 넘어간다.

그럼 아래와 같이 사용할 수 있다.

 

 

하지만 넘어가기 직전, BackPage가 이상하게 나타났다 돌아가는 버그가 있다.

이동은 DOTween으로, 위치 및 회전 계산은 Update에서 하며 나타난 버그로 추정된다. 이는 DOTween 대신 코루틴을 사용해, 직접 프레임 단위로 제어하며 고쳐봐야 겠다.


경계에 음영 추가

이제 음영을 추가해 좀 더 자연스럽게 만들어보자.

 

 

음영은 책이 접히는 부분에 추가해야 한다. 이는 Mask가 있으니 쉽게 할 수 있다.

 

마스크를 책 크기에 맞춰 새로 만들고, 음영의 위치와 각도는 Mask를 따라가게 한다.

 

// Mask의 이동할 거리 계산 및 이동
float maskX = (Vector2.Distance(point, corner) / 2) / Mathf.Cos(theta * Mathf.Deg2Rad);
mask[i].position = corner - new Vector3(maskX, 0f, 0f);
// Mask 회전
mask[i].rotation = Quaternion.Euler(0f, 0f, -theta);

// gradient도 함께 이동 및 회전
gradient[i].position = corner - new Vector3(maskX, 0f, 0f);
gradient[i].rotation = Quaternion.Euler(0f, 0f, -theta);

// 음영을 활성화한다.
gradient[i].gameObject.SetActive(true);

 

위와 같이 코드를 추가해줬다. 접기 전에 음영이 뜨는 것을 막기 위해, 기본 값을 비활성화로 둔 후 움직일 때 켜줬다.

 

 

간단하게 왼쪽처럼 계층 구조를 설정해줬다.

 

 

 

 

 

 

아직 그림자를 그리진 않아서, 간단하게 불투명도만 줬다. 제대로 이동하는 걸 볼 수 있다.


마무리

이로써 책 넘기는 효과를 구현하는 데엔 성공했다. 하지만 아직 갈 길이 멀다. 더 간결하고 나은 방법이 있을 수도 있고, 몇 가지 수정할 부분이 보이기도 하니까.

 

그러니 다음 포스팅에선, 구조를 개선하는 것으로 책 넘김 효과를 마무리 지어보자.