유니티(Unity3D)를 사용하여 게임을 만들다 보면 다른 게임오브젝트와 연동하여 동작해야 하는 경우가 빈번하게 발생합니다. 이번 포스팅에서는 다른 게임오브젝트를 제어하는 방법에 대하여 알아 보고자 합니다. 다른 게임 오브젝트의 트랜스폼을 변경하고, 하위에 있는 스크립트 컴포넌트의 필드(변수) 값을 변경하거나 읽어오고, 메소드(함수)를 원격으로 호출하는 방법에 관한 내용입니다.

 

본 포스트에서는 큐브 게임 오브젝트를 두 개 만들고 마우스 클릭이 이루어 졌을 때 상대 큐브를 회전 시키고, 색상을 변경하는 것을 시도할 것입니다.

 

우선 큐브를 두 개 생성하고, 이름을 각각 "Cube1"과 "Cube2"로 변경합니다. Cube1을 클릭 했을 시 Cube2가 조금시 회전할 것이며, Cube2의 색상이 변경되도록 할 예정입니다. 이때, Cube2의 색상이 빨간색이면 파란색으로 바꾸고, 파란색이면 빨간색으로 병경합니다.

 

이러한 동작을 위해서 Cube2에 탑재될 SetColor 클래스의 스크립트는 다음과 같이 작성합니다. SetColor 클래스의 필드는 settingColor를 가지고 있으며, 메소드(함수)로는 "Settingcolor()"라는 이름의 메소드(함수)를 가지고 있습니다.

using UnityEngine;
using System.Collections;

public class SetColor : MonoBehaviour {

	//변경할 생상 값을 저장할 settingColor라는 이름의  필드(변수)를 생성합니다. 
	//이 변수는 인스펙터 뷰에 나타나게 되어 색상을 입력 받을 수 있습니다.
	//초기 값은 Color.blue로 설정되었습니다.
	public Color settingColor = Color.blue;
	void Start () {

		SettingColor();
	}

	//다른 클래스에서 SettingColor함수를 호출할 것이기 때문에 접근한정자를 public으로 선언합니다.
	public void SettingColor()
	{
		//게임 오브젝트에 SettingColor 필드를 대입함으로써, 게임 오브젝트의 색상을 변경합니다.
		renderer.material.color = settingColor;
	}
}

이제는 Cube1에 탑재될 OtherObjectControl 클래스의 스크립트를 다음과 같이 작성합니다. 이 스크립트는 Cube1이 클릭되었을 때, Cube2를 회전 시키고, Cube2의 색상을 변경하는 스크립트 입니다. Cube2를 제어하기 위해 Find 메소드를 사용하여 Cube2를 찾고, 하위 컴포넌트를 찾기 위해 GetComponent메소드를 사용하는 방법을 잘 보시기 바랍니다.

using UnityEngine;
using System.Collections;

public class OtherObjectControl : MonoBehaviour {

	public GameObject controlObject; //이렇게 필드명을 public 선언하면 인스펙터 뷰에서 대상이 되는 
	//게임 오브젝트를 선택할 수 있습니다. 

	private SetColor component; //제어할 스크립트 클래스 타입의 참조변수 생성합니다.

	void Start()
	{
		//"Cube2"라는 이름의 게임 오브젝트를 찾아 controlObject라는 이름을 붙여줍니다.
		controlObject = GameObject.Find ("Cube2");
	}

	void OnMouseDown()
	{
		//대상 오브젝트 회전
		controlObject.transform.Rotate (0, 200 * Time.deltaTime, 0);

		//controlOject의 내장 컴포넌트 중 SetColor을 찾아 component라는 이름을 붙여줍니다.
		component = controlObject.GetComponent<SetColor>();

		//setColor의 소속된 필드 중 settingColor의 값이 Color.blue 인지 확인합니다.
		if (component.settingColor == Color.blue) {
			component.settingColor = Color.red; //Color.blue가 맞으면 Color.red로 변경합니다.
		}
		else {
			component.settingColor = Color.blue; //Color.blue가 아니면 Color.blue로 변경합니다.
		}
		component.SettingColor(); //SettingColor()메소드를 호출하여 색상변경을 실행합니다.
	}
}

각 스크립트를 Cube2와 Cube1에 탑재 시키고, 실행하한 후 Cube1을 클릭 시 Cube2의 회전과 색상이 변경되는 것을 확인하실 수 있을 것입니다. 끝.


 

블로그 이미지

로봇과나무

각종 다양한 종류의 게임들이 있지만 그 중에서 대중에게 전자 컴퓨터 게임의 세계를 알렸던 겔러그(Galaga, 1981, 남코, 위키백과)를 필두로 현재까지도 끊임없이 인기를 누리는 장르은 바로 슈팅(shooting) 게임일 것입니다. 슈팅게임의 화면을 구성하는 빠지지 않는 요소 중 하나는 바로 타겟(target) 모양의 마우스 커서(mouse cursor)를 들 수 있을 것입니다. 해서,타겟 모양의 마우스 커서를 Unity3D에서 설정하고, 게임화면상에서 사용하도록 하고자 합니다. 물론, 다른 장르의 게임에서도 게임의 성격을 잘 살릴 수 있는 여러가지 모양의 마우스 커서를 만들어 사용할 수 있을 것입니다.

먼저 가장 잘 사용할 수 있는 여러가지 그래픽 툴을 사용하여 커서로 사용할 이미지 파일을 제작합니다. 이때, 유니티 엔진 및 GPU(Graphic Processing Unit)는 2의 n승의 텍스처를 좋아하므로 32 x 32 나 64 x 64의 크기로 만드는 것이 좋습니다. 또한, 당연한 것이지만 빈 영역은 투명처리해야 하며, 완성된 파일은 PNG 형으로 저장하는 것이 좋습니다. 아래는 본인이 제작한 64 x 64의 크기를 갖는 타겟 모양의 그림입니다.

이제 모양이 준비되었으면, 유니티 프로젝트의 Textures 폴더에 저장하고, 다음 그림과 같이 유니티에서 해당 파일을 선택하여, 텍스처 임포트 셋팅(Texture Import Settings)를 합니다. 이때, Texture Type은 Cursor로, Wrap Mode는 Clamp로 그리고 Filter Mode는 Point로 합니다. 그리고 아래쪽의 Apply 버튼을 클릭합니다., 

 

이제, 커서로 사용할 텍스처가 준비되었습니다. 그러면 이제 커서를 변경 시켜 줄 스크립트를 만들 차례입니다. 우선 스크립트를 보기에 앞서, 인스펙터 뷰에 나타나게 될 스크립트 컴포넌트의 사용자 인터페이스를 보면서 설명하는 것이 나을 듯 합니다. 왜냐하면, 사용자 인터페이스를 보면 스크립트의 구성이 어떠할지 미리 짐작할 수 있기 때문입니다.

 

컴포넌트를 보시면 Mouse Cursor (Script)라는 이름의 컴포넌트 이며, 스크립트 타입이라는 것을 나타내고 있습니다. Cursor Texture라는 항목이 보입니다. 이곳은 마우스 커서로 사용할 텍스처를 입력 받는 곳입니다. 포스팅의 바로 윗 부분에서 텍스처의 타입을 Cursor로 설정해 놓은 그 텍스처를 선택하시면 됩니다. 그리고, Hot Spot Is Center라는 체크박스 항목이 있는데 이곳은 기본적으로 체크되어 있으며, 커서 텍스처의 중심이 마우스의 좌표가 될 것인지 아닌지 선택하는 부분입니다. 슈팅 게임에서 사용될 타겟 모양의 커서의 경우에는 이곳에 체크를 하여야 할 것입니다. 가끔은 타겟 모양의 커서라 할 지라도 정확히 중심이 아닌 곳을 마우스의 좌표로 설정할 필요성이 있는 경우가 있을 수 있습니다. 그럴 경우에는 Hot Spot Is Center의 체크를 제거하고 Adjust Hot Spot에 좌표를 입력하면 됩니다. Adjust Hot Spot의 X 값과 Y 값은 기본적으로 모두 0으로 설정되어 있습니다. 그것은 일반적인 화살표 모양의 커서는 화살촉의 끝 부분이 마우스의 좌표로 사용되기 때문입니다. 그럼, 이제 스크립트 코드를 살펴 볼까요?

using UnityEngine;
using System.Collections;

public class MouseCursor : MonoBehaviour {

	//마우스 포인터로 사용할 텍스처를 입력받습니다.
	public Texture2D cursorTexture ;
	//텍스처의 중심을 마우스 좌표로 할 것인지 체크박스로 입력받습니다.
	public bool hotSpotIsCenter = false;
	//텍스처의 어느부분을 마우스의 좌표로 할 것인지 텍스처의 
	//좌표를 입력받습니다.
	public Vector2 adjustHotSpot = Vector2.zero;
	//내부에서 사용할 필드를 선업합니다.
	private Vector2 hotSpot;
	public void Start(){

		//코루틴을 사용합니다. TargetCursor()함수를 호출합니다.
		StartCoroutine ("MyCursor");
	}
	//MyCursor()라는 이름의 코루틴이 시작됩니다.
	IEnumerator MyCursor() {

		//모든 렌더링이 완료될 때까지 대기할테니 렌더링이 완료되면 
		//깨워 달라고 유니티 엔진에 게 부탁하고 대기합니다.
		yield return new WaitForEndOfFrame();

		//텍스처의 중심을 마우스의 좌표로 사용하는 경우 
		//텍스처의 폭과 높이의 1/2을 hot Spot 좌표로 입력합니다.
		if (hotSpotIsCenter) {
			hotSpot.x = cursorTexture.width / 2;
			hotSpot.y = cursorTexture.height / 2;
		} else {
			//중심을 사용하지 않을 경우 Adjust Hot Spot으로 입력 받은 
			//것을 사용합니다.
			hotSpot = adjustHotSpot;
		}
		//이제 새로운 마우스 커서를 화면에 표시합니다.
		Cursor.SetCursor (cursorTexture, hotSpot, CursorMode.Auto);
	}
}

스크립트 코드에서 Start()함수에 Cursor.SetCursor (); 함수의 선언을 넣지 않고 별도의 코루틴 함수를 만들어 렌더링이 완료될때까지 기다린 후,  Cursor.SetCursor ();를 한 이유는 게임이 실행되는 기기의 성능에 따라서, 커서가 표시되지 않을 수 있기 때문입니다. 참고로 코루틴을 사용하지 않고 Invoke()함수를 호출하여 일정시간 대기 후 실행시키는 방법도 사용할 수 있습니다.

이제 스크립 코드도 완성되었으니, 하당 스크립트 컴포넌트를 메인 카메라나, 빈 게임오브젝트(Empty Game Object)에 등록한 후 사용하시면 되겠습니다. 아래 게임 화면은 제작한 타겟 모양의 마우스 커서를 적용한 후 갈무리 한 화면입니다.

항상 행운과 긍정적인 성과가 있으시길 기원합니다. 끝.

블로그 이미지

로봇과나무

슈팅게임 등 마우스 포인터의 방향으로 게임 오브젝트가 회전을 해야 할 경우가 있습니다. 본인의 경우에는 간단하게 LookAt(); 이라는 함수를 써볼 생각했었으나, 약간의 오류를 발견하고 좀 더 나은 방법이 없을까 조금 고민을 하고는, 삼각함수를 이용하여 직접 계산하여 회전을 구하기로 하였습니다. (나중에라도 응용하시라고, 수학적인 기본 설명을 조금 달았습니다만, 혹시나 어렵게 생각하시는 분은 그냥 아래 코드 부분으로 바로 가셔도 됩니다. 하지만, 이해하고 있어야 응용할 수 있습니다.)
 
먼저, 삼각함수에 대한 기본 지식이 필요합니다. 본 삼각함수는 중학교 수학시간 에 처음 접한 것이지만 그 이후로 계속해서 여러 좌표와 각도를 구하는 기본적인 수학이 되어 자주 사용되고 있습니다. 다들 아시겠지만, 간단히 정리하면 다음과 같습니다.
 
아래와 같이 직각삼각형이 주어졌을때, 각 선분의 길이를 구하는 식과 각 θ(쎄타)를 구하는 식은 다음과 같습니다. 즉, 각 θ(쎄타)와 선분의 길이 하나만 알면, 삼각함수를 사용하여 나머지 한 변의 길이를 알수 있으며, 반대로 두 변의 길이를 알면 역삼각함수를 이용하여 두 변이 이루는 각 θ(쎄타)를 구할 수 있습니다. 역삼각함수의 이름은 아래 식과 같이 -1승의 형식으로 나타낼 수도 있으며, '아크사인(arcsin)', '아크코사인(arccos)', '아크탄젠트(arctan)'라고 불려집니다.

 

우리가 구하려고 하는 것은 게임 오브젝트를 마우스 포인터가 위치한 곳으로 바라보게 하는것이 목적입니다. 해당 목적을 이루기 위해서는 Unity3D의 좌표계에 대한 이해를 먼저해야 하겠습니다. 유니티의 평면 좌표계는 다음 그림과 같으며, 각도는 일반 각과 함께 라디안을 함께 사용하고 있습니다. 입력과 출력 시 일반 각을 기준으로 이루어 지는지, 라디안 값이 기준이 되는지 항시 주의 깊게 살펴봐야 할 것입니다. 3시방향을 기준으로 하며, 12시 방향은 일반 각으로는 90도이며, 라디안 값으로는 0.5 π(파이)에 해당합니다. 9시 방향의 각도는 180도가 됩니다. 또한 라디안 값으로는 π(파이)에 해당합니다. 6시 방향은 -90도 이며, 라디안으로는 -0.5 π(파이)가 됩니다.

Unity3D의 좌표계에 대한 이해를 하시면 여러 용도로 활용하실 수 있기 때문에 간단하게 그림으로 정리해 드렸습니다. 각을 나타내는 수치들이 어떻게 변화되는지 이해하시면 끝입니다. 

이제 코드를 자세하게 들여다 보세요. 이해하기 쉽게 주석을 가능한 많이 달았습니다만, 잘 이해가 안되시는 분은 그냥 가져다 사용해 보시면 아시게 될 것입니다. 

public class MouseRotation : MonoBehaviour {

	void Update () {

		//먼저 계산을 위해 마우스와 게임 오브젝트의 현재의 좌표를 임시로 저장합니다.
		Vector3 mPosition = Input.mousePosition; //마우스 좌표 저장
		Vector3 oPosition = transform.position; //게임 오브젝트 좌표 저장

		//카메라가 앞면에서 뒤로 보고 있기 때문에, 마우스 position의 z축 정보에 
		//게임 오브젝트와 카메라와의 z축의 차이를 입력시켜줘야 합니다.
		mPosition.z = oPosition.z - Camera.main.transform.position.z; 

		//화면의 픽셀별로 변화되는 마우스의 좌표를 유니티의 좌표로 변화해 줘야 합니다.
		//그래야, 위치를 찾아갈 수 있겠습니다.
		Vector3 target = Camera.main.ScreenToWorldPoint(mPosition);

		//다음은 아크탄젠트(arctan, 역탄젠트)로 게임 오브젝트의 좌표와 마우스 포인트의 좌표를
		//이용하여 각도를 구한 후, 오일러(Euler)회전 함수를 사용하여 게임 오브젝트를 회전시키기
		//위해, 각 축의 거리차를 구한 후 오일러 회전함수에 적용시킵니다.

		//우선 각 축의 거리를 계산하여, dy, dx에 저장해 둡니다.
		float dy = target.y - oPosition.y;
		float dx = target.x - oPosition.x;

		//오릴러 회전 함수를 0에서 180 또는 0에서 -180의 각도를 입력 받는데 반하여
		//(물론 270과 같은 값의 입력도 전혀 문제없습니다.) 아크탄젠트 Atan2()함수의 결과 값은 
		//라디안 값(180도가 파이(3.141592654...)로)으로 출력되므로
		//라디안 값을 각도로 변화하기 위해 Rad2Deg를 곱해주어야 각도가 됩니다.
		float rotateDegree =  Mathf.Atan2(dy, dx)*Mathf.Rad2Deg;

		//구해진 각도를 오일러 회전 함수에 적용하여 z축을 기준으로 게임 오브젝트를 회전시킵니다.
		transform.rotation = Quaternion.Euler (0f, 0f, rotateDegree);
	}
}

사용법은 마우스의 방향에 따라 회전하고자 하는 게임 오브젝트에 컴포넌트로 등록하시면 됩니다. 참고로 본 코드는 카메라가 z축을 방향을 보고 위치해 있는 좌표을 기준으로 설명 된 것입니다. 카메라가 위에서 아래 방향으로 내려보고 싶다면 마우스의 축과 게임 오브젝트의 좌표 축이 맞지 않아 축에 대한 변경을 하셔야 할 것입니다.

모쪼록 많은 분들에게 도움이 되었으면 합니다. 끝.

 

블로그 이미지

로봇과나무

프로그램밍 작업을 하다보면 가끔은 코드 줄 번호를 확인해야 할 경우가 있습니다. 디버깅 과정이나, 코드를 다른이에게 설명할 때에는 더욱 필요함을 느끼게 됩니다.

아래는 이클립스(Eclipse)에서 줄 번호를 나오게 하는 방법입니다.


이클립스 메뉴에서 Window > Preferences 를 선택합니다.


Preferences에는 많은 항목들이 있습니다. "많이 당황하셨어요?" 그러나, 당황할 필요는 없습니다. 이클립스는 이곳에서도 필터기능을 지원하기 때문에 찾고자 하는 것이 무엇인지 알고만 있다면 keyword로 입력하면 바로 찾아서 보여 줍니다. 아래 그림은 Preferences에 진입했을 때의 화면 갈무리 이며, 좌측 상단에 보면 빨간 상자 안에 "type filter text"라고 입력을 기다리는 부분이 보입니다. 이곳에 찾고자 하는 키워드를 입력하고 엔터키를 누르면 찾고자 하시는 항목이 있는 곳으로 안내해 줄 것입니다.


그러면, 이제 우리가 찾고자 하는 것이 줄 번호(Line Number)이므로 "type filter text"라는 부분에 마우스를 누르고 "line number"를 입력한 후 엔터키를 눌러봅시다. keyword를 입력하는 도중에도 무엇인가 변화를 만들어 낼 것입니다. 신경쓰지 마시고, "line number"를 모두 입력한 다음에 엔터를 누르면 다음과 같이 줄 번호를 나타낼 수 있는 곳으로 안내해 줄 것입니다. 다음 갈무리에서 오른쪽 중간 부분에 "Show line numbers"라는 이름의 체크박스를 보실 수 있습니다. 이 체크박스에 체크한 다음에 가장 아래에 있는 OK 버튼을 눌러주면 됩니다.


이제 다음과 같이 작성한 프로그램 코드 좌측에서 줄 번호를 확인하실 수 있을 것입니다. 언제나 건강(마음건강, 몸건강) 챙기면서 즐거운 프로그램 작업하시길 바랍니다.


이상으로 이클립스에서 줄 번호를 나타내는 방법에 대하여 살펴봤습니다. 끝.



블로그 이미지

로봇과나무

How to change your MAC address on Raspberry Pi [2/2]

 

이번 포스팅에서는 이전 포스팅에서 예고한 바와 같이 배치파일을 이용하여 Booting시 네트웍인터페이스콘트롤러(이하 NIC)의 MAC 주소가 자동으로 변경되도록 하는 방법을 살펴보겠습니다.

 

/etc/network/ 아래에는 4개의 디렉토리가 존재합니다. 'if'로 시작하여 'd'로 끝나는 4개의 파란색 디렉토리가 보이네요.

if-down.d

if-post-down.d

if-pre-up.d

if-up.d

각각의 디렉토리는 네트웍인터페이스 카드가 down될 때, down된 후에, up되기 전에, up되면서 라는 의미를 담고 있으며, 해당되는 때에 디렉토리에 들어있는 배치파일을 실행하게 됩니다.

 

NIC의 MAC address를 변경하는 배치파일은 4개의 디렉토리 중 어디에  넣어주는 것이 좋을지 생각해 봐야 합니다. 우리가 지금 수행하려고 하는 작업이 NIC의 MAC address를 변경하는 것입니다. 가만히 생각해 보죠. MAC address를 변경한 후에 네트웍에 접속을 하는 것이 좋을 것 같습니다. 즉, 네트웍이 up되기 이전에 MAC address를 변경하는 것이 맞을 것입니다.

 

즉, if-pre-up.d 디렉토리 아래에 MAC address를 변경하는 배치파일을 만들어 넣어주면 됩니다. 이제 if-pre-up.d 아래에 vi, nano 등 즐겨서 사용하는 에디터로 배치파일을 만들어 보죠. 필자는 nano를 편집기로 사용하겠습니다.

 

$ sudo nano /etc/network/if-pre-up.d/nic-hw-addr-set

필자는 nic-hw-addr-set 이라는 이름의 배치파일을 만들 것입니다. 파일 이름은 임의로 넣어도 됩니다.

 

파일의 내용은 다음과 같이 적으면 됩니다. 아래 코드 중 변경하고자 하는 MAC 주소를 00:01:23:45:67:AB 부분에 적어주면 됩니다.

#!bin/sh
/sbin/ifconfig wlan0 hw ether 00:01:23:45:67:AB

만들어진 파일을 저장하고 에디터를 종료합니다.

 

여기서 잊지 말아야 한는 것이 실행권한을 부여해줘야 부팅시 자동으로 실행한다는 것입니다. 다음과 같이 'chmod'명령을 통하여 해당파일이 실행될 수 있도록 권한을 변경합니다.

$ sudo chmod 744 /etc/network/if-pre-up.d/nic-hw-addr-set

 

이제 시스템을 재시작 합니다.

$ sudo shutdown -r now

 

시스템을 재시작 한 후에는 해당 NIC의 MAC address가 잘 변경되었는지 확인해 봅니다.

$ ifconfig wlan0

 

다음과 같이 잘 변경된 것을 확인할 수 있습니다.

 

이것으로 NIC의 MAC address를 변경하는 방법에 대하여 살펴보았습니다. 감사합니다.

 

블로그 이미지

로봇과나무

How to change your MAC address on Raspberry Pi [1/2]

아주 가끔은 네트웍인터페이스 콘트롤러(이하 NIC, Network Interface Controller)의 하드웨어 주소를 변경할 필요가 발생하곤 합니다.

 

아래 코드 중 'wlan0'가 변경하려는 네트웍장치의 이름입니다. 무선랜카드의 경우는 wlan0, wlan1 등의 순서로 이름이 부여되고, 유선랜 장치의 경우에는 eth0, eth1 등의 순서로 이름이 부여됩니다. 하드웨어 주소를 변경하고자 하는 장치가 무엇인지 확인 한 후에 진행하여야 합니다. 본 포스팅에서는 wlan0의 MAC 주소를 변경하는 예시를 들겠습니다.

 

우선 해당 NIC의 동작을 중지시켜야 합니다.

Disable network

$ sudo ifconfig wlan0 down

 

해당 NIC의 MAC 주소를 변경합니다.

Change the MAC address of the interface

$ sudo ifconfig wlan0 hw ether 01:23:45:67:89:AB

 

변경 명령을 잘 수행하나요? 혹시 다음과 같은 메시지를 내 보내며 명령을 거부하지 않는지요?

아무런 문제를 일으키지 않은 경우에는 아래의 다음단계로 넘어가면 되겠습니다.

 

하지만 문제를 일으킬 경우에는 해당 NIC뿐만 아니라 시스템의 네트워킹을 중지시킨 후 변경을 재시도 해야 합니다. 이 경우 라즈베리파이에 모니터와 키보드가 연결된 상태에서는 문제가 없으나, SSH등으로 접속하여 명령을 내리는 경우에는 접속 자체가 끊겨 버리는 문제가 있어 어려움이 발생합니다. 이 경우에는 부팅 시에 자동으로 MAC 주소를 변경하도록 배치파일을 만들어 동작시키는 것이 좋습니다. 이렇게 배치파일을 이용하여 부팅 시에 MAC 주소를 변경하는 방법에 대하여는 다음 포스팅에서 다루도록 하겠습니다. 일단 이번 포스팅에서는 위와 같은 문제가 발생 않은 경우로 한정하여 진행하도록 하겠습니다.

 

위와 같은 오류 메시지가 발생하지 않았다면 해당 NIC를 동작시키면 되겠습니다.

Enable NIC

$ sudo ifconfig wlan0 up

 

이제 해당 네트웍 인터페이스 장치의 MAC 주소가 잘 변경되었는지 확인합니다.

Checking the MAC address

$ ifconfig wlan0

 

잘 되었습니까?

 

사실 중간 부분의 "SIOCSIFHWADDR: Device or resource busy - you may need to down the interface" 라는 메시지 때문에 그 이후에는 진행이 안되는 분들이 많을 것입니다. 필자 또한 그 문제로 진행이 안되었으니까요. 이러한 현상으로 진행이 안되신 분은 이어지는 포스팅을 확인해 보시기 바랍니다.

 

감사합니다.

 

블로그 이미지

로봇과나무