2023년 11월 4일 토요일

Unity TextMesh Pro 한글 깨지는 경우, 폰트 아틀라스 설명

 TextMesh Pro 를 사용할때 한글이 표기되도록 하는 방법입니다.


1. Font 를 구합니다.

https://gongu.copyright.or.kr/ 여기에서 쓸만한 폰트를 다운로드 받습니다.

학교안심 우주 R 이라는 폰트를 다운로드 받았습니다.


2. 폰트 파일을 Asset 폴더에 넣습니다.


3. 필요시 적절한 폴더로 이동합니다.

여기에서는 Font 폴더를 만들어서 이동하였습니다.


4. 폰트 에셋 만들기 메뉴를 선택합니다.


5. 폰트 아틀라스 생성하기



아틀라스란 스프라이트를 하나의 이미지로 만드는 작업입니다. 아틀라스를 만듦으로서 성능향상(드로우 콜이 작아짐)을 가져올 수 있기 때문에 비슷하게 전체 폰트에 대해서도 하나의 아틀라스 이미지를 생성하게 됩니다.
Sampling Point Size가 커지면 이미지 품질이 좀 더 좋아지게 되나 더 큰 아틀라스 이미지의 크기가 필요하게 됩니다.
여기 설명 안된건 기본값으로 사용합니다.

- SourceFontFile : 다운받은 폰트 선택

- Sampling Point Size : 자동으로 셋팅하거나 사이즈 지정해줌 아래에서는 32를 사용하였으나 해당 크기는 주로 사용하는 폰트의 크기와 비슷한 크기를 지정해 줍니다.

- Atlas Resolution : 아틀라스 크기인데 적절한 값을 정해줍니다. 일반적으로 4096 * 4096을 많이들 사용합니다만, 모바일 환경을 고려한다면 가능하면 작게 설정이 필요합니다. (너무 작게 설정시 한장의 아틀라스에 표현이 안될 수 있으니 확인 방법은 아래 설명을 참고 바랍니다.)

- CharacterSet : CustomRange

- Character Sequence 입력 : 32-126,44032-55203,12593-12643,8200-9900 한글에서는 일반적으로 이 구간의 문자만 사용합니다.
- GenerateFontAtlas 클릭하고 Save버튼 클릭하면 .asset 확장자의 폰트가 생성됩니다.



 만약 설정을 아틀라스 크기가 작거나 샘플링 크기가 크거나 등등 모든 폰트를 하나의 아틀라스 이미지에 표현하지 못한다면 위 스크린샷 마지막 항목 Excluded characters:0 이라고 표기된 부분에 0이 아닌 숫자가 나타나게 됩니다. 해당 경우에는 숫자 값을 조절해야 합니다.

6. TextMesh Pro Object 에서 Font Asset 선택하기



Ubuntu docker, Package 'docker-ce' has no installation candidate

Ubuntu docker

아래 명령어를 입력하여 리눅스를 업데이트해줍니다.

sudo apt-get update
sudo apt-get upgrade


도커를 설치합니다.

sudo apt install docker-ce


docker-ce 패키지를 사용할 수 없습니다.
'docker-ce' 패키지는 설치할 수 있는 후보가 없습니다.


위와 같은 오류시 조치 방법

$ sudo apt-get update
$ sudo apt-get install \
   ca-certificates \
   curl \
   gnupg \
   lsb-release
   
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io



2023년 10월 29일 일요일

Making Block Stack Game with Unity (블럭 쌓기 게임 제작)

Block Stack Game?

스택 게임이란 무언가를 높게 쌓아올리는 퍼즐류의 게임입니다.
유명한 젠가라는 게임과 무너질때까지 진행한다는 점에서는 비슷하면서도 게임 방식은 반대가 되는 게임입니다. 블럭을 떨어뜨리면서 무너지지 않을때까지 진행하는 게임입니다.
여기에서는 2D형태로 Unity를 이용하여 제작하였습니다.


1. 라이센스

CC0
모든 코드는 자유롭게 수정 배포할 수 있습니다.


2. 동작 영상



3. 이미지




4. 떨어지는 블럭 만들기


block Image를 Scene로 Drag & Drop 한뒤 다시 해당 오브젝트를 Assets으로 가져오면 Prefab Asset이 만들어 집니다.
그리고 나서 몇가지 콤포넌트를 추가했습니다.

4.1 컴포넌트들

Transform, Sprite Renderer는 기본으로 추가 되고 Rigidbody 2D, Box Collider 2D 는 물리 게임에서는 필수라서 추가가 되었고 DownBlock 이라는 스크립트를 추가 했습니다.
대부분 기본값을 사용하였으며 Rigidbody 2D 에서는 움직이는 물체라서 Dynamic으로 변경하였습니다.

4.2 DownBlock.cs


using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class DownBlock : MonoBehaviour
{
    Transform tf = null;
    Rigidbody2D rg = null;
    public float maxScale = 3.0f;
    public bool rightMove = true;
    public float moveSpeed = 4.0f;
    public bool drop = false;
    public bool needCheck = false;
    public static string gameState = "playing";

    // Start is called before the first frame update
    private void Awake()
    {
        tf = GetComponent<Transform>();
        tf.localScale = new Vector3(0f, 0f, 0f);
        rg = GetComponent<Rigidbody2D>();
        rg.gravityScale = 0f;
        drop = false;
        needCheck = false;
    }

    // Update is called once per frame
    void Update()
    {
        if (gameState == "gameend")
        {
            GetComponent<Rigidbody2D>().gravityScale = 1f;
            drop = true;
        }
        if (tf.localScale.x >= maxScale && drop==false && (Input.GetKey(KeyCode.Space) || Input.GetMouseButton(0)))
        {
            GetComponent<Rigidbody2D>().gravityScale = 1f;
            drop = true;
        }
    }

    void FixedUpdate()
    {
        Vector3 scale = tf.localScale;
        bool havetomove = false;
        if (!drop)
        {
            if (scale.x < maxScale)
            {
                scale.x = scale.x + 8f * Time.fixedDeltaTime;
                scale.y = scale.x;
            }
            else
            {
                scale.x = maxScale;
                scale.y = scale.x;
                havetomove = true;
            }
            tf.localScale = new Vector3(scale.x, scale.y, 0f);
            if (havetomove)
            {
                Vector3 pos = rg.position;                
                if (rightMove)
                {
                    pos.x = pos.x + moveSpeed * Time.fixedDeltaTime;
                }
                else
                {
                    pos.x = pos.x - moveSpeed * Time.fixedDeltaTime;
                }
                rg.MovePosition(new Vector3(pos.x, pos.y, pos.z));
            }
        }
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("Wall"))
        {
            if (rightMove)
            {
                rightMove = false;
            }
            else
            {
                rightMove = true;
            }
        }

        else if (collision.gameObject.CompareTag("DeadWall"))
        {
            gameState = "gameover";
            Destroy(gameObject);
        }
    }
    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("DontCare"))
        {
            needCheck = true;
        }
    }
}


4.3 변수 설명

maxScale : pixel로 만들다 보니 크기가 작습니다. 화면에 너무 작게 나와서 확대해서 표현이 되는데 적절한 크기인 3.0로 정했습니다.
또한 처음 신규 block이 나타날때는 크기를 0에서부터 시작해서 3.0까지 scale을 증가시켜서 자연스럽게 등장 하는 효과를 보기 위한 용도도 있습니다.
rightMove : 생성된 벽은 최초 해당값에 따라 이동이 됩니다. rightMove가 true다면 처음부터 오른쪽으로 이동하다가 벽에 막히면 방향을 바꾸어서 이동하게 하는 변수 입니다.
moveSpeed : Block의 이동 속도입니다.
drop : 사용자가 키를 눌러서 아래로 떨어지는 블럭임을 표시하는 변수 입니다.
needCheck : 블럭이 떨어지면 블럭이 움직이지 않아야만 다음 블럭이 나타나게 됩니다. 처음 생성된 블럭은 x, y 좌표로 움직을 주기 때문에 속도가 나타나지 않습니다. drop을 시작해야만 속도가 발생하게 됩니다. 그래서 초기에 생성된 속도가 없는 블럭들은 검사대상에서 제외하기 위해 만들어진 변수 입니다. true 인 블럭만 블럭이 멈췄는지 검사를 하게 됩니다.
gameState : 게임이 진행중인지 종료인지 상태를 나타냅니다. static 변수라서 전 class에서 하나의 영역에만 존재하는 값입니다.

4.4 코드 설명


    private void Awake()
    {
        tf = GetComponent<Transform>();
        tf.localScale = new Vector3(0f, 0f, 0f);
        rg = GetComponent<Rigidbody2D>();
        rg.gravityScale = 0f;
        drop = false;
        needCheck = false;
    }
초기화 코드를 Awake에 넣었습니다. Object Pool을 고려해서 만든것이긴한데 Start쪽에 넣어도 무관합니다. 각종 변수들을 초기화 하는 코드입니다. 

tf.localScale = new Vector3(0f, 0f, 0f);
처음 나타난 블럭은 scale이 0이라 눈에 보이지 않게 됩니다.

        if (gameState == "gameend")
        {
            GetComponent<Rigidbody2D>().gravityScale = 1f;
            drop = true;
        }
위 코드는 게임이 끝나게 되면 혹시 남아있는 블럭이 있다면 drop을 시작하게 만들기 위해서 넣었습니다.

        if (tf.localScale.x >= maxScale && drop==false && (Input.GetKey(KeyCode.Space) || Input.GetMouseButton(0)))
        {
            GetComponent<Rigidbody2D>().gravityScale = 1f;
            drop = true;
        }
키보드 버튼이나 마우스 클릭을 한다면 중력을 설정합니다.

다음은 FixedUpdate() 함수 설명입니다.
주된 내용은 drop 이 아닐때만 동작시킵니다. drop 이 시작되면 gravity를 1로 설정하면 움직임이 자동으로 생성되기 때문입니다.
            if (scale.x < maxScale)
            {
                scale.x = scale.x + 8f * Time.fixedDeltaTime;
                scale.y = scale.x;
            }
이 부분은 현재 scale이 정해놓은 max 보다 작다면 일정크기로 증가시키는 코드입니다. 물론 x, y scale을 동일하게 하기 위해서 scale.y = scale.x 와 같은 형태로 처리하였습니다.

tf.localScale = new Vector3(scale.x, scale.y, 0f);
위 코드는 이미지의 스케일을 변경하는 코드입니다.

                scale.x = maxScale;
                scale.y = scale.x;
                havetomove = true;
최대 scale이 되었다면 그때 부터는 좌우 이동을 하게 됩니다. 해당 용도로 havetomove = true; 변수를 사용하게 됩니다.

            if (havetomove)
            {
                Vector3 pos = rg.position;                
                if (rightMove)
                {
                    pos.x = pos.x + moveSpeed * Time.fixedDeltaTime;
                }
                else
                {
                    pos.x = pos.x - moveSpeed * Time.fixedDeltaTime;
                }
                rg.MovePosition(new Vector3(pos.x, pos.y, pos.z));
            }
좌 우 움직임을 처리하는 코드입니다. rightMove true이면 오른쪽 이동을 하게 되고 false이며녀 왼쪽 이동을 하게 됩니다.
rightMove 값을 변경하는 곳은 충돌 처리에서 할 예정입니다.

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("Wall"))
        {
            if (rightMove)
            {
                rightMove = false;
            }
            else
            {
                rightMove = true;
            }
        }
벽을 감지하게 되면 진행 방향을 반대로 바꿔주게 됩니다.


"Wall" tag로 Object를 만들어 상단 좌우에 배치하였습니다. 그리고 Main Camera의 자식으로 삼았는데요. 이것은 블럭이 쌓이게되면 메인 카메라를 위쪽으로 조금씩 이동하게 되는데 그때 블럭도 같이 이동시키기 위함입니다.

Wall 은 기본 EmptyObject에 Box Collider 2D를 붙인상태에서 Is Trigger check 하고 위치와 크기만 조정한 것입니다.


        else if (collision.gameObject.CompareTag("DeadWall"))
        {
            gameState = "gameover";
            Destroy(gameObject);
        }
"DeadWall"과의 충돌은 게임 중지를 나타냅니다.

DeadWall 위치는 다음과 같습니다.

좌우의 deadwall은 Main Camera 하위에 있고 바닥의 DeadWall은 dummyObject 인 main 하위에 있습니다. 아래쪽 dead wall은 카메라 이동시 움직이지 않도록 처리했습니다.


DeadWall도 마찬가지로 기본 EmptyObject에 Box Collider 2D를 붙인상태에서 Is Trigger check 후 위치와 크기만 조정한 것입니다. 물론 Tag 설정은 추가로 해줘야 합니다. (위 스크린샷 참고)

DontCare Tag
앞서 needCheck 변수에 관해서 설명을 할때 해당 값이 true인 경우에만 블럭의 정지 여부를 판단해서 다음 블럭이 나오도록 처리한다고 했었습니다. 그렇다면 해당 구현을 어떻게 해야할까요?

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("DontCare"))
        {
            needCheck = true;
        }
    }

OnTriggerExit 이벤트에 해당 내용을 추가하여 "DontCare" tag 영역을 블럭이 벗어날때 needCheck 값을 true로 변경하였습니다.


위 스크린샷에서 빨간색 사각형이 DontCare Tag 블럭의 위치입니다.


5. 플랫폼 설치

떨어지는 블록을 받아줄 단단한 바닥을 만들 차례입니다.
platform 이미지를 끌어와서 Platform 이라는 이름을 붙여줍니다. 그리고 적당한 위치에 옮겨 놓습니다.




위와 같이 처리하고 Rigidbody 2D 컴포넌트를 추가하고 Body Type을 Static으로 변경합니다. 추가로 Box Collider 2D도 추가해줍니다.

6. Egg 포인트

spawn 이나 egg로 표현하는데 어떤 물체가 생성하는 장소를 말합니다. 이 게임에서는 다음 block이 생성되는 장소로 정의 하였습니다. 위치만 사용할 예정이므로 다른 컴포넌트는 필요없습니다.


대략 이 정도 위치에 아무것도 없이 추가하였습니다. 추가시 주의할점은 Main Camera 자식위치로 설정하였습니다. 또한 구별하기 쉽게 특별히 아이콘도 넣어주었습니다.

7. GameManager

EmptyObject를 생성 후 이름을 변경해서 스크립트를 GameManager라는 이름으로 만들었습니다. 이곳에서 관리를 위한 여러가지 처리를 담당하도록 구현하였습니다.

7.1 GameManager.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;

public class GameManager : MonoBehaviour
{
    public static GameManager instance;
    public GameObject genObj;
    public GameObject startPanel;
    public GameObject gameoverPanel;
    public GameObject scoreText;
    public Transform genPosition;
    public float invokeTimer = 1.0f;
    int score = 0;

    float mainCamTargetY = 0f;
    // Start is called before the first frame update
    void Start()
    {
        instance = this;
       DownBlock.gameState = "playing";
if (gameoverPanel) { gameoverPanel.SetActive(false); } if (invokeTimer > 0f) { Invoke("InvokeFunction", invokeTimer); Invoke("InvokeStartImageDeActive", invokeTimer); } score = 0; } // Update is called once per frame void Update() { if (DownBlock.gameState == "playing") { GameObject[] block = GameObject.FindGameObjectsWithTag("Player"); float maxVelocy = -1f; float maxY = -100f; int dropFalseCount = 0; int blockCount = 0; for (int i = 0; i < block.Length; i++) { DownBlock downBlock = block[i].GetComponent<DownBlock>(); Rigidbody2D rd = block[i].GetComponent<Rigidbody2D>(); if (rd != null) { if (!downBlock.drop) { dropFalseCount++; continue; } else if (!downBlock.needCheck) { dropFalseCount++; continue; } else { if (maxVelocy < rd.velocity.magnitude) { maxVelocy = rd.velocity.magnitude; } if (maxY < rd.position.y) { maxY = rd.position.y; } blockCount++; } } } GameObject mainCam = GameObject.FindGameObjectWithTag("MainCamera"); if (block.Length > 0 && dropFalseCount == 0 && maxVelocy < 0.1f) { { InvokeFunction(); if (maxY + 2 > mainCam.transform.position.y) { mainCamTargetY = maxY + 2; } score = blockCount; } } if (mainCamTargetY > mainCam.transform.position.y) { mainCam.transform.position = new Vector3(mainCam.transform.position.x, mainCam.transform.position.y + 0.5f * Time.deltaTime, mainCam.transform.position.z); } } else if (DownBlock.gameState == "gameover") { if (gameoverPanel) { gameoverPanel.SetActive(true); } } if (scoreText) { scoreText.GetComponent<Text>().text = score.ToString(); } } void InvokeFunction() { GameObject result; result = Instantiate(genObj); result.transform.position = genPosition.transform.position; } void InvokeStartImageDeActive() { if (startPanel) { startPanel.SetActive(false); } } }

7.2 변수 설명

    public static GameManager instance; // 다른곳에서 GameManager 변수를 쉽게 접속하기 위해서 만들어 뒀는데 이번 예제에서는 사용하지 않았습니다.
    public GameObject genObj; // 생성을 위한 block ( Prefab 된 block 을 선택해줍니다.) 을 의미합니다. 
    public GameObject startPanel; // 게임을 시작하면 Start Game이라는 글자가 나오는 UI 판넬입니다.
    public GameObject gameoverPanel; // 게임이 종료될때 나타나는 Game over 판넬입니다. 이곳에는 재시작 버튼이 포함되어 있습니다.
    public GameObject scoreText; // 상단에 점수를 출력하는 UI 입니다.
    public Transform genPosition; // 생성되는 위치 즉, Egg 포인트를 의미합니다.
    public float invokeTimer = 1.0f; // 게임을 시작하면 곧장 시작하는것이 아닌 일정한 간격을 두고 시작하는데 게임 시작 delay를 주기위한 시간입니다. 
    int score = 0; // 현재 점수를 나타냅니다.
    float mainCamTargetY = 0f; // 카메라 Y 위치를 조정하기 위한 변수입니다. 여기에 설정된 값을 기준으로 카메라가 해당값과 같아질때까지 조금씩 이동을 합니다. (MainCamera는 최초 0위치에 있기 때문에 해당값도 0이 됩니다.)


Pulbic 변수를 어떻게 할당했는지 위 Screenshot 확인하기 바랍니다.


7.3 코드 설명


시작 코드는 아래와 같이 특정 조건이 맞을때 초기화 하는 것으로 구성되어 있습니다.
처음 시작시 gameoverPanel 은 있다면 SetActive(false) 로 꺼주도록 합니다.
그리고 gameState도 playing 상태로 만들어줄 필요가 있습니다.

    void Start()
    {
        instance = this;
       DownBlock.gameState = "playing";
if (gameoverPanel) { gameoverPanel.SetActive(false); } if (invokeTimer > 0f) { Invoke("InvokeFunction", invokeTimer); Invoke("InvokeStartImageDeActive", invokeTimer); } score = 0; }


            Invoke("InvokeFunction", invokeTimer);
            Invoke("InvokeStartImageDeActive", invokeTimer);

이 부분은 일정 시간 invokeTimer 후에 앞에 있는 함수를 실행시켜주는 역할을 합니다.

    void InvokeFunction()
    {
        GameObject result;
        result = Instantiate(genObj);
        result.transform.position = genPosition.transform.position;
    }
    void InvokeStartImageDeActive()
    {
        if (startPanel)
        {
            startPanel.SetActive(false);
        }
    }

1초 후에 startPanel 과 Prefab에 있는 block을 생성시키는 동작을 합니다. 


8. StartPanel


StartPanel은 UI>Panel 을 이용해서 만들었습니다.
그리고 아래에 UI>Image 를 이용해서 이미지를 넣었습니다.

1초 후에 SetActive(false) 이 호출되어 이미지가 사라지게 됩니다.

생성된 object는 result로 들어오고 계층도에서 최상위에 생성이 됩니다. (BlockEgg 자식으로 생성되는것이 아닙니다.) result.transform.position = genPosition.transform.position; 이 코드를 이용해서 위치만 해당 위치로 이동시켜 줍니다.

실제 동작되는 모습에서 확인이 가능합니다.

이와 같이 구현한 이유는 BlockEgg가 Main Camera 하위에 있기 때문에 Camera 이동시 문제가 발생하기 때문입니다.

8.1 Update함수

해당 함수내에 코드는 block 이 이동이 멈췄다면 다음 블럭이 나오도록 하는 코드와 점수 계산 카메라 이동을 담당합니다.

            GameObject[] block = GameObject.FindGameObjectsWithTag("Player");
            float maxVelocy = -1f;
            float maxY = -100f;
            int dropFalseCount = 0;
            int blockCount = 0;
            for (int i = 0; i < block.Length; i++)
            {
                DownBlock downBlock = block[i].GetComponent<DownBlock>();
                Rigidbody2D rd = block[i].GetComponent<Rigidbody2D>();
                if (rd != null)
                {
                    if (!downBlock.drop)
                    {
                        dropFalseCount++;
                        continue;
                    }
                    else if (!downBlock.needCheck)
                    {
                        dropFalseCount++;
                        continue;
                    }
                    else
                    {
                        if (maxVelocy < rd.velocity.magnitude)
                        {
                            maxVelocy = rd.velocity.magnitude;
                        }
                        if (maxY < rd.position.y)
                        {
                            maxY = rd.position.y;
                        }
                        blockCount++;
                    }
                }
            }
위 코드는 모든 블럭을 찾아서 drop true 이고 needCheck 인 블럭들만 모아서 blockCount 를 하게 되면 그것이 점수가 됩니다. 또한 해당 블럭들이 Rigidbody2D 의 velocity.magnitude를 산출해냅니다. 이 값을 이용해서 블럭이 멈췄는지 알아 내게 되는데 모든 block중에 가장 큰값을 알아내고 y 좌표도 가장 큰 값을 알아내는 코드입니다.

            GameObject mainCam = GameObject.FindGameObjectWithTag("MainCamera");
            if (block.Length > 0 && dropFalseCount == 0 && maxVelocy < 0.1f)
            {
                {
                    InvokeFunction();
                    if (maxY + 2 > mainCam.transform.position.y)
                    {
                        mainCamTargetY = maxY + 2;
                    }
                    score = blockCount;
                }
            }
            if (mainCamTargetY > mainCam.transform.position.y)
            {
                mainCam.transform.position = new Vector3(mainCam.transform.position.x, mainCam.transform.position.y + 0.5f * Time.deltaTime, mainCam.transform.position.z);
            }

이번 코드는 maxVelocy 가 특정값 보다 작게 되면 block들이 멈춰 있다고 판단하고 새로운 블럭을 생성 (InvokeFunction();) 하게됩니다. 그리고 지금까지 블럭을 카운트한 갯수가 점수가 됩니다. ( score = blockCount; )
앞서 block들의 가장 큰 y 값을 구했습니다. 모든 블럭들이 멈춘 상태에서 maxY가 카메라 위치까지 올라왔다면 카메라를 올려야 하는 위치 (mainCamTargetY)를 설정해줍니다. 여기에서는 대략 +2 로 설정 하였습니다.

mainCam.transform.position = new Vector3(mainCam.transform.position.x, mainCam.transform.position.y + 0.5f * Time.deltaTime, mainCam.transform.position.z);

그리고 위코드는 조금씩 카메라 위치를 이동 시키는 코드 입니다.

8.2 게임 종료 상태

게임이 종료가 되면 gameoverPanel 을 보여줍니다. 여기에는 재시작 버튼도 나타납니다.
Canvas는 기본적으로 Hidden 상태로 만들어서 게임 Scene방해가 되지 않도록 하였습니다.
GameOverPanel은 UI>Panel 을 이용해서 만들었습니다.
그리고 아래에 UI>Image 와 UI>Legacy>Button을 추가하였습니다.
대략 아래와 같이 구성됩니다.


        else if (DownBlock.gameState == "gameover")
        {
            if (gameoverPanel)
            {
                gameoverPanel.SetActive(true);
            }
        }

코드는 게임 종료시는 SetActive(true)를 해주고 게임 시작시는 SetActive(false) 로 처리합니다.

9. Scene 이동

UI>Legacy>Button 을 이용해서 만든 버튼에 Scene 이동 동작을 넣는 방법입니다.


조금 복잡할 수 있는데 ChangeScene.cs 스크립트를 만들고 해당 버튼에 스크립트 컴포넌트를 연결하고 On Click() 부분에 RestatButton을 끌어와서 놓아야 Load() 함수가 나타나게 됩니다. 컴포넌트 인자는 이동하고자하는 Scene를 넣습니다. 이 게임은 Scene 이름은 다음과 같기 때문에 StackPlayScene 값을 입력 하였습니다.

9.1 ChangeScene.cs


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

public class ChangeScene : MonoBehaviour
{
    public string sceneName;
    public void Load()
    {
       SceneManager.LoadScene(sceneName);
    }
}



10. 스코어 표기


text object는 UI>Legacy>Text로 만들어 주면 됩니다.



        if (scoreText)
        {
            scoreText.GetComponent<Text>().text = score.ToString();
        }

score 표시는 위와 같이 해줍니다. 

11. 전체소스

URP 프로젝트에서 동작 시켜야 합니다.



2023년 10월 22일 일요일

Python list compare (리스트 비교)

 Python에서 List내부에 값을 포함하는 경우 단순히 '==' "비교 연산자 만으로도 잘 될까?"에 대한 궁금증이 생겼습니다.

실제 테스트 해보도록 하겠습니다.


1. 상수 값으로 이루어진 가장 기본적인 테스트 입니다.

>>> a = [ 1, 2, 3]
>>> b = [ 1, 2, 3]
>>> a==b
True

2. list 순서가 변경된다면 비교가 어떨까요?

>>> a = [ 1, 2, 3]
>>> b = [ 3, 2, 1]
>>> a==b
False

리스트는 순서가 있겠죠? 그러니 당연히 안됩니다.


3. list내에 dict 타입이 들어 있는 경우 입니다.

그런데 순서가 뒤죽박죽 되어 있다면?? 이번에도 안될까요?

>>> a = [{'a':1,'b':2}]
>>> b = [{'a':1,'b':2}]
>>> a==b
True
>>> b = [{'b':2,'a':1}]
>>> a==b
True
>>> a
[{'a': 1, 'b': 2}]
>>> b
[{'b': 2, 'a': 1}]

잘 동작 됩니다. key, value 쌍만 잘맞으면 dict 타입은 순서가 중요하지 않습니다.





2023년 10월 19일 목요일

Unity Creator Kit - RPG build error


Assets\Creator Kit - RPG\Scripts\Tiles\Isometric Rule Tile\Editor\IsometricRuleTileEditor.cs(8,26): error CS0433: The type 'IsometricRuleTile' exists in both 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' and 'Unity.2D.Tilemap.Extras, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'

->IsometricRuleTile 두곳에 있어서 발생하는 현상, 한쪽을 삭제 필요.

IsometricRuleTile.cs 파일 삭제

위치 : Assets\Creator Kit - RPG\Scripts\Tiles\Isometric Rule Tile\IsometricRuleTile.cs


Assets\Creator Kit - RPG\Scripts\Tiles\Isometric Rule Tile\Editor\IsometricRuleTileEditor.cs(19,32): error CS0115: 'IsometricRuleTileEditor.RuleMatrixOnGUI(RuleTile, Rect, RuleTile.TilingRule)': no suitable method found to override

->한참을 검색 찾아봤지만 해결했다는 사람을 못찾았습니다.

해당 코드가 게임을 동작시키는 곳의 코드는 아닌것 같아서 문제가 되는 함수를 삭제했습니다.


Assets\Creator Kit - RPG\Scripts\Tiles\Isometric Rule Tile\Editor\IsometricRuleTileEditor.cs(72,32): error CS0507: 'IsometricRuleTileEditor.ContainsMousePosition(Rect)': cannot change access modifiers when overriding 'public' inherited member 'RuleTileEditor.ContainsMousePosition(Rect)'

->이 부분도 해당 코드가 게임을 동작시키는 곳의 코드는 아닌것 같아서 문제가 되는 함수를 삭제했습니다.


에러나는 함수를 모두 삭제하면 해당 파일은 아래와 같습니다.

Assets\Creator Kit - RPG\Scripts\Tiles\Isometric Rule Tile\Editor\IsometricRuleTileEditor.cs 

using System;
using System.Linq;
using System.Reflection;
using UnityEngine;

namespace UnityEditor
{
    [CustomEditor(typeof(IsometricRuleTile2), true)]
    [CanEditMultipleObjects]
    internal class IsometricRuleTileEditor : RuleTileEditor
    {
        private static readonly int[, ] s_Arrows =
        {
            { 3, 0, 1 },
            { 6, 9, 2 },
            { 7, 8, 5 },
        };

    }
}

그리고 재시작해보니 Play도 잘 동작합니다.



2023년 10월 14일 토요일

python requests timeout 의 고찰

requests 모듈을 사용하다가 timeout이 발생하여 관련해서 찾아보고 정리해봤습니다.

Timeout 의 기초

문서에서는 아래와 같이 나와있습니다.

https://requests.readthedocs.io/en/stable/user/advanced/#timeouts

보통은 timeout 을 설정하지 않고 사용하는 경우 timeout이 얼마나 발생하는지 궁금해서 문서를 찾아봤는데 정보가 애매하게 되어있습니다.

시간 초과가 없으면 코드가 몇 분 이상 중단될 수 있습니다. 

시간 정보가 구체적이지 않았습니다.

그래서 몇가지 테스트를 해봤습니다. 아래와 같이 코드를 구현해서 timeout이 발생할 수 있는 조건으로 시험해봤습니다.

import requests
import datetime
import traceback
import time


def url_connect(url, timeout=None):
    nowtime = datetime.datetime.now()
    try:
        print(f"********** trying connect {url} *************")
        if timeout != None:
            r = requests.get(url, timeout=timeout)
        else:
            r = requests.get(url)
        print(r)
    except Exception as e:
        print("end time:", datetime.datetime.now() - nowtime)
        time.sleep(1)
        print("EXCEPTION", e)
        traceback.print_exc()


if __name__ == "__main__":
    url_connect("http://1.1.1.1:9999")
    url_connect("http://192.168.0.254")
    url_connect("http://127.0.0.1")
    url_connect("http://192.168.0.254", timeout=10)


********** trying connect http://1.1.1.1:9999 *************
end time: 0:00:21.042910
EXCEPTION HTTPConnectionPool(host='1.1.1.1', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
TimeoutError: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 186, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='1.1.1.1', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout.py", line 14, in url_connect
    r = requests.get(url)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 565, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='1.1.1.1', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
********** trying connect http://192.168.0.254 *************
end time: 0:00:21.032674
EXCEPTION HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
TimeoutError: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 186, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout.py", line 14, in url_connect
    r = requests.get(url)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 565, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
********** trying connect http://127.0.0.1 *************
end time: 0:00:02.053697
EXCEPTION HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
ConnectionRefusedError: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 186, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout.py", line 14, in url_connect
    r = requests.get(url)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 565, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))
********** trying connect http://192.168.0.254 *************
end time: 0:00:10.010868
EXCEPTION HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 179, in _new_conn
    raise ConnectTimeoutError(
urllib3.exceptions.ConnectTimeoutError: (<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout.py", line 12, in url_connect
    r = requests.get(url, timeout=timeout)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 553, in send
    raise ConnectTimeout(e, request=request)
requests.exceptions.ConnectTimeout: HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)'))

call stack 부분을 빼고 다시 정리해봤습니다.

********** trying connect http://1.1.1.1:9999 *************
end time: 0:00:21.042910
EXCEPTION HTTPConnectionPool(host='1.1.1.1', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
********** trying connect http://192.168.0.254 *************
end time: 0:00:21.032674
EXCEPTION HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
********** trying connect http://127.0.0.1 *************
end time: 0:00:02.053697
EXCEPTION HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))
********** trying connect http://192.168.0.254 *************
end time: 0:00:10.010868
EXCEPTION HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)'))

test code가 크게 4가지 인데요.

1.1.1.1:9999 의 주소는 응답을 안할꺼라는 가정하에 테스트 해봤습니다.

21초 걸렸고 Max retries exceeded 트라이가 초과되었다? 라는 애매한 표현을 하고 있습니다.

두번째는 내부 공유기 쪽으로 안쓰는 포트에 시도해보았습니다.

마찬가지로 21초가 걸렸습니다.

세번째 127.0.0.1 은 내부 포트로 2초만에 응답이 없습니다. timeout이 아니고 거부 형태입니다.

마지막은 timeout을 10초로 넣고 두번째 테스트 했던 내부 공유기 주소로 사용해봤습니다.

10초에 Max retries exceeded 부분은 동일하나 뒤쪽에 보면 timed out 이 발생함을 알 수 있습니다.

위 테스트에서 21초가 걸리긴 했지만 그것을 내부 기본 timeout이라고 할 수는 없습니다. 내부적으로 소켓에 대한 타임 아웃이 있고 그걸 retry 하는 형태로 구현이 되어 있음을 알 수 있으며, 우리가 설정하는 timeout 과는 Exception 이 다름을 알 수 있었습니다.


사용법

타임 아웃이 필요한경우는

r = requests.get('https://github.com', timeout=5)

위와 같이 사용해주면 되는데, 2가지 종류의 시간을 설정이 가능합니다. connect, read 시간입니다. connect timeout은 일반적으로 listen 하고 있는 서버에 접속하면 accept (연결) 를 하게되는데 그때까지의 시간 이라고 보면 됩니다.

The connect timeout is the number of seconds Requests will wait for your client to establish a connection to a remote machine (corresponding to the connect()) call on the socket.

read 시간은 데이터를 주고 받을때의 시간입니다. 클라이언트가 서버에서 전송된 바이트 사이에 대기하는 시간(초)입니다.

(Specifically, it’s the number of seconds that the client will wait between bytes sent from the server. In 99.9% of cases, this is the time before the server sends the first byte).

r = requests.get('https://github.com', timeout=(3.05, 27))

timeout=(connect timeout, read time) 형태로 설정합니다.

무한정 기다리는 경우 아래와 같이 합니다. 그런데 앞에서 테스트 해봤듯이 꼭 무한정이 되는것은 아닙니다.

r = requests.get('https://github.com', timeout=None)


Timeout 을 넘어서

여기에서 한가지 궁금한 점이 생겼습니다. requests에서 파일을 받거나 web page를 받을때의 timeout은 전체 크기를 받는 부분의 timeout 인가라는 의문입니다.

예를 들어 100MB의 파일을 받는 상황이고 requests.get의 timeout이 1초 일때 100MB를 1초만에 받지 못하면 timeout이 발생하는가입니다.

그래서 예제를 만들어 봤습니다.

웹 서버를 구성해야하는 예제이므로 복잡하지만 간단하게 예제를 위해서 socket 으로 설계했습니다.


import socket


IP = '0.0.0.0'
PORT = 8080
ADDR = (IP, PORT)


def make_dummy_message(len):
    ret = ""
    for i in range(0, len):
        ret = ret + 'A'
    return ret

head_data = f"""HTTP/1.1 200 OK

Server: Werkzeug/2.2.2 Python/3.8.10
Date: Sat, 14 Oct 2023 01:43:25 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 100000000
Connection: close

<!doctype html>
<html lang=en>
"""

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind(ADDR)
    server_socket.listen()
    print(f"listen [{ADDR}]")

    while True:
        client_socket, client_addr = server_socket.accept()
        print(f"accept [{client_addr}]")
        # msg = client_socket.recv(SIZE)
        print(head_data)
        client_socket.sendall(head_data.encode())
        for i in range(1, 100):
            print(f"send {i}")
            client_socket.sendall(make_dummy_message(1000000).encode())

        client_socket.close()

web 소켓을 열어서 http 응답을 주는 형태입니다. make_dummy_message 에 의해서 약 1MB의 메세지를 client_socket.sendall 함수로 전달해 줍니다.

        for i in range(1, 100):
            print(f"send {i}")
            client_socket.sendall(make_dummy_message(1000000).encode())

1MB씩 100번 100MB가 전달되게 됩니다.


이제 client 코드 입니다.

import requests
import datetime
import traceback
import time
import socket

def url_connect(url, timeout=None):
    nowtime = datetime.datetime.now()
    try:
        print(f"********** trying connect {url} *************")
        if timeout != None:
            r = requests.get(url, timeout=timeout)
        else:
            r = requests.get(url)
        print(r)
    except Exception as e:
        print("end time:", datetime.datetime.now() - nowtime)
        time.sleep(1)
        print("EXCEPTION", e)
        traceback.print_exc()


if __name__ == "__main__":
   s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   s.connect(("8.8.8.8", 80))
   myip = s.getsockname()[0]
   print(myip)
   s.close()
   url_connect(f"http://{myip}:8080", timeout=1)

나의 ip를 구한다음 8080 포트로 접속해 보는것입니다.

서버를 실행하지 않은 상태는 1초만에 아래 형태로 나타납니다.

192.168.0.35
********** trying connect http://192.168.0.35:8080 *************
end time: 0:00:01.013626
EXCEPTION HTTPConnectionPool(host='192.168.0.35', port=8080): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001A957F37670>, 'Connection to 192.168.0.35 timed out. (connect timeout=1)'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 179, in _new_conn
    raise ConnectTimeoutError(
urllib3.exceptions.ConnectTimeoutError: (<urllib3.connection.HTTPConnection object at 0x000001A957F37670>, 'Connection to 192.168.0.35 timed out. (connect timeout=1)')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='192.168.0.35', port=8080): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001A957F37670>, 'Connection to 192.168.0.35 timed out. (connect timeout=1)'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout3.py", line 12, in url_connect
    r = requests.get(url, timeout=timeout)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 553, in send
    raise ConnectTimeout(e, request=request)
requests.exceptions.ConnectTimeout: HTTPConnectionPool(host='192.168.0.35', port=8080): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001A957F37670>, 'Connection to 192.168.0.35 timed out. (connect timeout=1)'))

서버를 실행한 다음에는 다른 에러가 발생하는데

192.168.0.35
********** trying connect http://192.168.0.35:8080 *************
end time: 0:00:11.814797
EXCEPTION ("Connection broken: ConnectionResetError(10054, '현재 연결은 원격 호스트에 의해 강제로 끊겼습니다', None, 10054, None)", ConnectionResetError(10054, '현재 연결은 원격 호스트에 의해 강제로 끊겼습니다', None, 10054, None))

대략 위와 같이 11초 후에 에러같은 것이 발생합니다.

몇가지 이유가 있는데 서버쪽을 대충 만들어서 그렇습니다. contents크기를 받고 소켓을 닫도록 되어있는데 해당 부분이 적절치 않게 만들었기 때문입니다.

timeout시간만 보려고 테스트 코드를 만든것이라 에러에 대해서는 무시하면됩니다.

11초가 걸렸는데도 1초 timeout이 발생하지 않았다는 의미는 구간 구간 socket read 시 1초 timeout이 발생할때만 멈춤다는 것을 알 수 있습니다.