Quaternion
을 생성합니다.요약
목적 : 파리가 원 운동과 다음 경로로의 이동을 자연스럽게 하게 하는 것
전 날 Lerp를 이용한 이중 보간으로 계산한 경로를 따라 이동시켜보았으나,
움직임이 자연스럽지 않아 다른 방법을 모색해봄.
다음 목적지를 정할 때 이전 도착점에서부터 일정 거리 이하인 점들을 고르되,
화면에서 벗어난 점을 선택했다면 목적지를 다시 고르는 방식을 고려해보았으나,
edge case에서 비효율이 발생할 수 있을 것 같아 기각
원하는 느낌의 경로를 그려본 뒤, 원 궤도와 직선 경로의 조합으로 단순화
구체적인 요구 사항과 수식을 칠판에 적음
특정 점을 향하고 크기가 고정된 벡터와 그 직교 벡터를 더하여 운동시킨다면
가속도를 갖지 않고 자연스럽게 움직일 수 있을 것이라 예상하고, 구현함.
중심점은 씬에 영역을 지정할 오브젝트를 둔 뒤 SpriteRenderer의 Bound 범위 내에서
매번 랜덤하게 고르게 함.
경로를 지정할 때 화면 바깥으로 나가지 않도록
랜덤 경로 지정할 때 화면 바깥이면 다시 계산하게 한다면
어쩌다 화면 맨 구석으로 갔을 때 1/4에 가까운 확률을 뚫어야 다시 돌아올 수 있음
4번이나 다시 계산해야 한다는 뜻
파리 한 마리면 괜찮겠지만 엄청 많을 땐?
물론 내 게임은 엄청 많이 하진 않겠지만
어제의 문제 : 일반적인 파리의 움직임이 아니었다.
원형으로 선회하면서 매끄럽게 움직이게 하고 싶은데
과한 포물선 운동을 하면서 뭔가 어색하게 느껴짐
영역 안의 랜덤 점에 원을 그려놓고 해당 영역에 들어서면 일정 각도 이상 빙글 돌도록?
해당 영역에 들어섰는지 안 들어섰는지 판단에 대한 연산 비용이 너무 비쌀듯
모든 점은 카메라가 비추는 화면 안에 있다 (간단하게 영역 설정하고 바운드 가져올거임)
각 점을 중심으로 작은 원이 그려지고, 최소한 270도는 돌고 해당 원을 탈출할 것이다.
점과 점 사이를 오갈 때의 경로는 1차 베지어 곡선 혹은 2차 베지어 곡선을 그리는데,
베지어 곡선의 핸들은 탈출했을 때의 원의 접선과 같다.
2차 베지어 곡선의 중간점은 원의 중심점 사이의 중점이고, 중간점 핸들의 각도는 랜덤이다.
만약 rigidbody가 있는 오브젝트라면 transform.position은 지양하는것이 좋습니다.
이걸 사용하면 모든 콜라이더들이 rigidbody와의 상대적 위치를 재계산하기 때문입니다.
상황에따라서 RigidBody.position이나 RigidBody.MovePosition()을 사용합시다.
딸깍으로 하자면 애셋 스토어에서 스플라인 플러그인 찾아보거나, iTween, LeanTween같은 Tweening 라이브러리 쓰면 된다고는 하는데 일단 여기까지 왔으니까 마저 구현해보기.
원 돌 때도 등속력으로 운동하게 하고 싶으니까 원 따라 트랜스폼만 움직이는게 아니라
수식을 궤도 운동 느낌으로 바꾸면 될듯
일단 이거 먼저 구현해보기
[SerializeField] private GameObject Sun;
private Vector2 fall_v;
private Vector2 moving_v;
[SerializeField] private float speed = 2f; // 속도 조절 변수
[SerializeField] private float grav = 4f;
void Update()
{
fall_v = (Sun.transform.position - transform.position).normalized;
moving_v.x = -fall_v.y;
moving_v.y = fall_v.x;
moving_v = moving_v * speed;
fall_v *= grav;
transform.position += (Vector3)(moving_v + fall_v) * Time.deltaTime;
}
됐다.
움직임을 강제로 직교 벡터로 만들어서 무조건 원 운동 하게 만듦.
Time.deltaTime 안 곱했더니 프레임 단위로 움직여서 개빨리 움직였는데 추가하니까 부드럽게 움직임.
근데 이렇게 하니까 궤도가 살짝씩 비틀어질 때도 있긴 한데 중요한 건 아니고 오히려 랜덤하게 움직여서 좋으니까 패스.
Direct Position Manipulation: You're adding the gravitational effect (fall_v) directly to the position, rather than applying it as an acceleration affecting velocity. This bypasses the physics of motion and leads to unnatural movement.
gpt는 실제 물리를 안 따라서 코드가 이상하다고 하는데, 오히려 중력 적용하면 더 이상해질듯? 난 그냥 원 궤도 그리려고 한 거라..
void Update()
{
fall_v = (Sun.transform.position - transform.position).normalized;
moving_v = new Vector2(-fall_v.y, fall_v.x) * speed;
fall_v *= grav;
transform.position += (Vector3)(moving_v + fall_v) * Time.deltaTime;
}
리팩토링 함.
다음으론 베지어 곡선 구현해보자.
라고 생각했었는데, 그냥 원 운동에 필요한 수치들만 랜덤으로 잘 조절하면
비슷한 효과 낼 수 있을듯?
새로운 중심점이 생성되는 위치도 스폰 영역 안에서만.
파리를 프리팹으로 만들거라 스폰 영역을 어떻게 할당시켜줘야 할까 고민하다가
그냥 일단 태그 먹인 뒤에 FindWithTag 쓰기로.
GameObject.FindWithTag("SpawnArea");
이건 왜 꼭 앞에 GameObject를 붙여야 됨?
GameObject.FindWithTag("SpawnArea")에서 GameObject를 붙이는 이유는 FindWithTag가 GameObject 클래스의 정적 메서드이기 때문입니다. Unity에서는 특정 클래스에 속한 정적 메서드를 호출할 때, 그 클래스 이름을 앞에 붙여 호출해야 합니다. GameObject가 제공하는 여러 정적 메서드(Find, FindWithTag, Instantiate, Destroy 등)도 같은 방식으로 사용됩니다.
Start()에서 초기화하고 랜덤 위치도 바로 구하다가
랜덤 위치 구하는 건 함수로 따로 뺐는데 작동을 안 함.
void Start()
{
sec_count = 0;
SpawnArea = GameObject.FindWithTag("SpawnArea");
Debug.Log("SpawnArea : " + SpawnArea.name);
Bounds SpawnBounds = SpawnArea.GetComponent<SpriteRenderer>().bounds;
Center = RandomCenter();
}
Bounds SpawnBounds
이렇게 로컬 변수로 선언해버려서 그랬던 거.
전역 변수로 뺐는데도 자료형을 앞에 써버리니까 네임스페이스가 달라진듯.
float sqrDistance = (targetObject.position - transform.position).sqrMagnitude;
거리가 매우 빈번하게 체크되어 성능이 중요한 경우 sqrMagnitude를 사용해 제곱 거리로 비교할 수 있습니다. 이 방식은 제곱근 계산을 피할 수 있어 성능에 유리합니다.
값 둘 중 하나 선택하는 랜덤 함수는 없냐
int RandomChoice(int value1, int value2)
{
return Random.Range(0, 2) == 0 ? value1 : value2;
}
C#에서는 두 값 중 하나를 무작위로 선택할 수 있는 기본 함수는 없지만, 간단히 구현할 수 있습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RandomFlying : MonoBehaviour
{
private Vector3 fall_v;
private Vector3 moving_v;
[SerializeField] private Vector3 Center;
[SerializeField] private float speed = 20f;
[SerializeField] private float grav = 10f;
private float sec_count;
private GameObject SpawnArea;
private Bounds SpawnBounds;
// Start is called before the first frame update
void Start()
{
sec_count = 0;
SpawnArea = GameObject.FindWithTag("SpawnArea");
SpawnBounds = SpawnArea.GetComponent<SpriteRenderer>().bounds;
Center = RandomCenter();
}
void Update()
{
sec_count += Time.deltaTime;
float sqrDistance = (Center - transform.position).sqrMagnitude;
if (sec_count > 0.4f || sqrDistance < 0.1f)
{
sec_count = 0;
Center = RandomCenter();
speed = RandomChoice(20, -20);
// Debug.Log("New Center : " + Center);
}
// 중심에 대해 원운동 시키기
fall_v = (Center - transform.position).normalized;
moving_v = new Vector3(-fall_v.y, fall_v.x) * speed;
fall_v *= grav;
transform.position += (moving_v + fall_v) * Time.deltaTime;
}
private Vector3 RandomCenter()
{
float randomX = Random.Range(SpawnBounds.min.x, SpawnBounds.max.x);
float randomY = Random.Range(SpawnBounds.min.y, SpawnBounds.max.y);
return new Vector3(randomX, randomY, 0);
}
int RandomChoice(int value1, int value2)
{
return Random.Range(0, 2) == 0 ? value1 : value2;
}
}
https://www.youtube.com/watch?v=Cz9tlhSD7XQ
간단한 스트레치 애니메이션
https://www.youtube.com/watch?v=F_LtpgpTHA8
이건 스크립트로 좀 더 고급지게 구현
근데 이것들은 내가 원하는 건 아닐지도.
https://www.youtube.com/watch?v=sKB72i1cXrc
이게 내가 원하는 건데 애셋을 사야됨.
https://www.youtube.com/watch?v=_Kjg5YMHCF0
이건 셰이더 그래프로 하는거.
You can make your texture repeatable in import settings then change the tiling in your material.
https://www.reddit.com/r/Unity3D/comments/nyxvr1/repeat_texture_instead_of_scaling/?rdt=62924
간단하게 타일링 할 수도 있음
https://docs.unity3d.com/kr/2021.3/Manual/9SliceSprites.html
근데 9slice 하면 나쁘지 않게 나올 것 같아서 이걸로 해보기로
대충 그리고
오 좋음 이거임
이 두 가지가 있을텐데
당연히 연산으로만 보면 2번이 효율적이겠지만
문제는 '점까지'라는 걸 어떻게 판단하는지가 문제.
width 1만큼 늘리면 x 좌표 1 늘린 거하고 똑같으니까
sin cos 써서 어떻게 어떻게 하면 될듯?
생각해보니까 생각보다 간단함.
// 클릭 당시 혀와 마우스의 위치를 설정하고 각도를 구한다.
tonguePos = transform.position; // TODO :: tongue 따로 할당해서 위치 설정해야 함
mousePos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
direction = mousePos - tonguePos;
angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
// 각도만큼 혀를 돌린다
transform.rotation = Quaternion.Euler(new Vector3(0, 0, angle));
마우스 클릭한 곳에 혓바닥 돌리기 완료
// 클릭 당시 혀와 마우스의 위치를 설정하고 각도를 구한다.
tonguePos = tongue.transform.position;
mousePos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
direction = mousePos - tonguePos;
direction.z = 0; // magnitude 왜곡 방지
targetLength = direction.magnitude;
mousePos의 z좌표가 -10이어서 magnitude가 왜곡됨.
혀가 원하는 것보다 길어지는 문제 발생. 0으로 설정해서 해결.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class FrogAttack : MonoBehaviour
{
private GameObject tongue;
private SpriteRenderer tongueSR;
private PlayerInput playerInput;
private InputAction AttackAction;
private bool isAttacking;
private bool arrivedPoint;
private Vector3 tonguePos;
private Vector3 mousePos;
private Vector3 direction;
private float angle;
private float targetLength;
[SerializeField] private float tongueSpeed = 10f;
void Awake()
{
tongue = gameObject; // TODO :: tongue 따로 할당해서 위치 설정해야 함
tongueSR = tongue.GetComponent<SpriteRenderer>();
// PlayerInput 컴포넌트에서 InputAction 가져오기
playerInput = GetComponent<PlayerInput>();
AttackAction = playerInput.actions["Attack"];
}
// 혀의 위치로부터 마우스 클릭 위치까지 각도를 구한 뒤에,
// 각도만큼 돌린 후에 거리만큼 늘리면 됨.
// 혀 공격 중이라면 점에 닿을 때까지 늘려야 하고
// 혀 공격이 달성됐다면 다시 줄여야 함
// 늘리거나 줄이는 건 Time.deltaTime만큼
void Update()
{
// 혀 공격은 혀를 늘리는 동작과 줄이는 동작으로 나뉜다
if (isAttacking)
{
// 점에 도착하지 못했다면 혀를 늘리고
if (!arrivedPoint)
StretchTongue();
// 점에 도착했다면 혀를 줄인다
else
ShrinkTongue();
}
else if (AttackAction.triggered)
{
// 클릭 당시 혀와 마우스의 위치를 설정하고 각도를 구한다.
tonguePos = tongue.transform.position;
mousePos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
direction = mousePos - tonguePos;
direction.z = 0; // magnitude 왜곡 방지
targetLength = direction.magnitude;
angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
// Debug.Log("tongePos : " + tonguePos);
// Debug.Log("direction : " + direction);
// Debug.Log("targetLength : " + targetLength);
// 각도만큼 혀를 돌린다
tongue.transform.rotation = Quaternion.Euler(new Vector3(0, 0, angle));
isAttacking = true;
arrivedPoint = false;
}
}
private void StretchTongue()
{
if (tongueSR.size.x >= targetLength)
{
arrivedPoint = true;
return;
}
tongueSR.size = new Vector2(tongueSR.size.x + tongueSpeed * targetLength * Time.deltaTime, tongueSR.size.y);
}
private void ShrinkTongue()
{
if (tongueSR.size.x <= 0)
{
isAttacking = false;
tongueSR.size = new Vector2(0, tongueSR.size.y);
return;
}
tongueSR.size = new Vector2(tongueSR.size.x - tongueSpeed * targetLength * Time.deltaTime, tongueSR.size.y);
}
}
N=int(input())
for i in range(N):
for j in range(N):
temp_i, temp_j = i, j
flag = False
if temp_i % 3 == 1 and temp_j % 3 == 1:
flag = True
if not flag:
while temp_i >= 3 and temp_j >= 3:
temp_i //= 3
temp_j //= 3
if temp_i % 3 == 1 and temp_j % 3 == 1:
flag = True
break
if flag:
print(" ",end="")
else:
print("*", end="")
print("")
최적화 해봤는데
역시 시간초과.
import sys
print = sys.stdout.write
N=int(input())
for i in range(N):
for j in range(N):
temp_i, temp_j = i, j
flag = False
if temp_i % 3 == 1 and temp_j % 3 == 1:
flag = True
else:
while temp_i >= 3 and temp_j >= 3:
temp_i //= 3
temp_j //= 3
if temp_i % 3 == 1 and temp_j % 3 == 1:
flag = True
break
if flag:
print(" ")
else:
print("*")
print("\n")
풀어본 사람들이 와서 보더니
print 출력 느리니까 바꿔보라고 해서 바꿨더니 일단 통과는 함
def draw_stars(n):
if n==1:
return ['*']
Stars=draw_stars(n//3)
L=[]
for star in Stars:
L.append(star*3)
for star in Stars:
L.append(star+' '*(n//3)+star)
for star in Stars:
L.append(star*3)
return L
N=int(input())
print('\n'.join(draw_stars(N)))
근데 분할 정복 이용해서 훨씬 빨리 푼 풀이도 있었다.
뭔소린가 했는데 해설 보니까 바로 대충 이해가 가긴 함
코드 더 보다 보니까 완전히 이해
*3
은 가로로 늘리는 거고
for문 세 번 쓰는 건 세로로 늘리는 거