지난 포스팅에선 위 부분까지 구현했었다.
이제 음영을 넣어 자연스레 만들고, 페이지를 늘려 책 한 권을 완성시켜보자.
페이지 늘리기
원래 음영을 먼저 하는 게 자연스러운데, 까먹고 늘리는 걸 먼저 해버렸다. (이러면 나중에 음영을 일일이 추가해줘야 한다.)
먼저 프리팹화하고, 하이어라키 창에 여럿으로 늘렸다.
그리고 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);
위와 같이 코드를 추가해줬다. 접기 전에 음영이 뜨는 것을 막기 위해, 기본 값을 비활성화로 둔 후 움직일 때 켜줬다.
간단하게 왼쪽처럼 계층 구조를 설정해줬다.
아직 그림자를 그리진 않아서, 간단하게 불투명도만 줬다. 제대로 이동하는 걸 볼 수 있다.
마무리
이로써 책 넘기는 효과를 구현하는 데엔 성공했다. 하지만 아직 갈 길이 멀다. 더 간결하고 나은 방법이 있을 수도 있고, 몇 가지 수정할 부분이 보이기도 하니까.
그러니 다음 포스팅에선, 구조를 개선하는 것으로 책 넘김 효과를 마무리 지어보자.
'Unity > 로직 설계' 카테고리의 다른 글
[Unity] CSV에서 자동으로 에셋 생성하기 (4) | 2024.08.28 |
---|---|
[Unity] 책 넘기는 효과 구현하기 (Page Curl) - 3 (0) | 2024.06.01 |
[Unity] 책 넘기는 효과 구현하기 (Page Curl) - 1 (0) | 2024.05.31 |
[Unity] InputAction 내 이벤트 초기화 (0) | 2023.02.24 |
[Unity] DOTween - 회전하지 않고 물체 주위 맴돌기 (0) | 2022.11.06 |