3/10

 

이렇게 좋은 아이디어로 이렇게 망칠 수 있나. 그래서 더 아쉬운 영화.

파묘는 토속신앙을 다뤄서 일본귀신을 때려잡는 영화라고 볼 수 있다. 토속신앙을 다루는 영화중 이렇게 흥행이 된 영화는 없지 않을까 싶다. 그만큼 나도 기대해서 봤지만 아주 기대를 깎아 먹었다.

 

박지용의 역할은 무엇인가

박지용(배우 김재철)이 파묘를 의뢰했지만 김상덕(배우 최민식)은 묫자리가 너무나도 안좋아 거절하면서 차에 탔는데 맞은편에서 박지용이 최민식을 뚫어지게 보는 장면을 보고 나는 박지용에게 무언가 있다라고 생각했다. 결국 다시 박지용, 김상덕, 이화림(배우 김고은) 셋이서 대화를 나누면서 김상덕은 박지용에게 숨기는게 있냐고 물었더니 없다고 대답했다. 그러다가 결국 파묘가 성사 되었다. 대살굿 장면은 인상적이긴 하지만 와닿지는 않는다. 박지용은 파묘후 묘는 개관을 하지않고 그대로 화장해달라고 했고, 개관을 하지 않는 이유에 대해선 대답하지 않는다. 보국사 보살에 의해 묘에 대한 이야기가 나오는데 거기엔 금은보화가 있을거라고 했으니 박지용이 개관하지 말라는 이유는 금은보화가 있기 때문이라고 우리는 간접적으로 이해할 수 있다.

한명의 욕심으로 개관을 했더니 금은보화랑 상관없이 묘에서 혼령이 빠져나와 스토리가 진행되는것을 볼 수 있다. 그럼 우리는 다시 생각해 볼 수 있다. 박지용은 혼령의 존재를 알고 있어서 개관을 하지 말라고 한것인가? 그렇다면 박지용은 어디까지 알고 있는 존재인가? 그러나 전혀 아니다. 박지용이 혼령의 존재를 알고있으리라는 증거도 없으며 그 혼령에게 죽어버리기 때문. 

이 모든 과정은 전혀 연결이 되지 않는다.

1. 파묘를 하지 않겠다는 김상덕을 뚫어지게 보는 박지용.

2. 이유는 말하지 않고 개관을 하지 말라는 박지용.

3. 묘에 금은보화가 있다는 얘기가 나오지만 전혀 상관없는 혼령이 등장.

4. 그 혼령에게 죽는 박지용.

결국 박지용은 우리에게 의문만 남긴채 죽어버린다. 혹은 정말 의미가 없은 박지용의 행동이다. 그저 이야기를 전진시키기 위한 하나의 도구였을 뿐.

 

'여자얼굴의 뱀'은 무엇인가

약간 과거로 돌아가서 파묘후 한명의 인부가 삽으로 '여자얼굴의 뱀'을 죽여 비명소리만 남기는데 이것은 본격적인 스토리가 흘러간다는 뜻이지만, '왜?'라는 질문이 당연히 나온다.

1. 왜 거기에 '여자얼굴의 뱀'이 있었는가?

2. 첩장이여서 있었다면 왜 첩장엔 '여자얼굴의 뱀'이 있어야 하는가?

3. '여자얼굴의 뱀'의 비명소리가 본격적인 스토리의 시작을 알렸다면 굳이 이 방법을 쓰는 이유가 무엇인가?

영화 내에선 이 질문에 대답이 없다. 아무리 강렬한 장면이지만 의미가 없다. 

 

혼령은 실체가 있는것인가

개관후 튀어나온 혼령은 후손인 박지용일가를 죽이려고 한다. 마치 혼령은 투명인간처럼 물리적인 능력이 있어보인다. 그러나 박지용의 형과, 두번의 유산 그리고 박지용의 아들은 위험한 상황에 있는데, 이것은 개관하기 전부터 그랬던 것이다. 다시 말하면 개관으로 인해 혼령이 빠져나오지 않았어도 혼령은 충분히 박지용일가에게 나쁜 영향을 줄 수 있었던 것이고 그것은 물리적인 실체가 없어도 가능한 일이기 때문에 지역에 상관없이 동시다발적으로 일어날 수 도 있다고 볼 수 있다. 하지만 빠져나온 혼령은 천천히 한명씩 죽이게 되며 박지용의 아이는 천천히 죽이려다 혼령이 당하게 된다...

1. 혼령이 빠져나오기 전은 힘이 약했던가?

2. 혼령이 빠져나오면 어떤 방식으로 물리적인 능력을 갖게 되는것인가?

3. 혼령은 김상덕의 목소리를 흉내내고 폰이 없어도 전화할 수 있는 능력도 있는것은 어디서 온건가?

개관은 그저 혼령이 쎄져서 영화를 단순히 급박하게만든 역할 그이상도 그이하도 아니다.

 

챕터의 역할은 무엇인가

영화는 6개의 챕터가 있는데 챕터의 존재 이유를 알 수가 없다. 챕터가 없었어도 이야기는 충분히 흘러갈 수 있고 오히려 챕터 때문에 다른것에 집중하게 만든다.

동티챕터에서 '여자머리의 뱀'을 통해 첩장이란것을 알게 되는데 동티 챕터는 결국 첩장의 이야기로 구성되어 있다. 그럴꺼면 챕터흐름을 동티로 잡고 하던가 첩장이라는 챕터를 쓰던가 챕터 이름이 첩장인 것 자체가 스포라면 다른 이름을 쓰던가 아니면 챕터를 쓰지 않거나 하는게 더 좋았다고 생각이 되는데 잘 모르겠다. 챕터에서 비중없이 다루는 이야기를 챕터이름으로 쓰는것은 난 잘 모르겠다. 

 

두개의 묘

우린 두개의 묘를 봤다. 박지용조상의 묘와 첩장. 이것은 한국귀신과 일본귀신의 차이를 보여주며 한국귀신은 원한만 풀어주면 해결이 되고, 일본귀신은 원한과 상관없이 '악'만 가지고 있다. 이것은 영화에서도 중요한 부분인데... 그만큼 중요하다는 것은 보여주지 못한것 같다. 결국 박지용조상의 원한을 풀어주지 못한채 묘를 태워서 없앴고, 일본귀신도 그냥 죽여버렸는데 한을 풀어주는게 중요한가 싶다. 

 

고증

영화에서 고증은 그저 선택지중 하나다. 특정한 장르(역사 등)를 제외하면 고증을 지키지 않고 논리적으로 설득만 된다면 문제는 없다. 고증을 지키는 선택을 했다면 고증에 대한 재미를 관객은 느껴야한다. 

파묘는 고증을 아주 잘 지키는 선택을 했는데 정작 나는 이유를 모르겠다. 나는 설득되지 않았다. 영화에서 보여주는 토속신앙의 행동들의 대부분은 설명없이 진행한다. 대살굿의 준비물 및 과정, 김상덕이 묫자리에 100원을 던지거나 등등.. 이런것들은 우리가 어렴풋이 알 수 있다. '아~ 대살굿은 저렇게하는거고 파묘를 하면 100원을 던지는것이 상도구나~' 그러나 몇몇부분은 이해가 되지 않는 부분이 있는데 이것이 고증인지 아닌지 헷갈리게 한다.

1. 일본귀신은 왜 은어와 참외를 원했는가? 이 부분에 대해선 언급이 없다. 분명 고증인데 최소한의 설명이 없으니 이해가 되지 않는 부분.

2. 일본귀신은 윤봉길(배우 이도현)을 죽이려고 할 때 왜 축경을 피해서 공격했는가? 그렇다면 김상덕의 축경은 왜 피하지 않고 그대로 공격했는가?

영화 내에선 설명이 없으니 도무지 이해가 안간다. 이런점에서 고증자체는 중요하지않고 맥락이 논리적으로 설득이 되는점이 제일 중요하다고 본다.

파묘의 고증이 잘 되어있다고 했지만 정작 나는 어디부터 어디까지 고증인지 모르겠고 납득도 되지 않는다.

 

 

파묘는 좋은 소재라서 너무너무너무너무너무 아쉽다. 참 아쉽다... 장재현감독은 검은사제들,사바하같은 오컬트 영화를 감독한 경험이 있는 감독인데도 불구하고 파묘를 왜 이렇게까지 밖에 못한지 참 아쉽다. 

 

 

'영화 감상평' 카테고리의 다른 글

테넷 감상평 스포있음  (0) 2024.09.07

10/10

 

크리스토퍼 놀란은 메멘토, 프레스티지, 인셉션, 인터스텔라등 시공간이 얽힌 이야기를 과학적으로 잘 이야기를한다. 오펜하이머처럼 과학이 중요한 내용이지만 영화적 재미까지 놓치지 않는다.

 

테넷을 이미 본 사람들은 항상하는말이 '이해하기 어렵다' 한발 더 앞서면 '이해가 안되서 무슨얘기인지 모르겠다'라는 말까지 나오는데 내가 직접 테넷을 보니 테넷은 이해가 꼭 필요한 영화가 아니란것을 알았다. 오히려 테넷은 클리셰가 섞여있어서 인셉션, 인터스텔라처럼 과학에 미친 영화가 이해에 초점들 두면 안된다.

 

메멘토는 시간이 계속 바뀌기 때문에 시간관계를 계속 인지하면서 영화를 봐야하지만, 테넷은 인버전이라는 시간이 반대방향으로 흐른다는 것만 알고있으면 영화를 이해하는데 큰 문제는 없다. 물론 영화를 보는 동시에 이해하는것이 더 재미는 있겠지만 그것이 필수는 아니고 완벽히 이해하지 못해도 충분히 재미가 있다는것을 말하고 싶다.

개인적으론 엔트로피와 시간의 관계에 대해서 어느정도만 알고있으면 좋을듯 하다.

 

내가 많은 영화를 본건 아니지만 대부분의 시간여행 장르는 과거든 미래든 하나의 시공간으로 이동한다. 예를들어 미래를 가고싶으면 '2100년1월1일 1시 서울'처럼 시간과 장소를 지정하고 어떤 기계에 탑승해 도착하는데, 테넷은 엔트로피 이론을 가져와 시간 역행(시간 역재생)이라는 개념을 사용했다. 완전 처음인 개념은 아니고 'Primer'라는 영화해서 사용이 된 개념이지만 이것을 더욱 확대했다고 보면된다.

 

단순히 시공간이동은 이동후에도 '인간의 시간방향'과 동일하기 때문에 할아버지 역설등 기존의 시간과 관련된 패러독스만 해결하면 되는 문제였다. '슈타인즈게이트'라는 애니처럼 시공간이동으로 일어나는 문제를 가장 합리적으로 우리에게 설명하는것이 목표인데, 테넷은 '인간의 시간방향의 반대'라는 시간 역행이라는 개념을 사용해서 기존에 볼 수 없었던 새로운 것들을 볼 수 있고 우리는 거기에서 놀란에게 감탄한다.

 

영화 초반부에 주도자와 닐이 프리포트에 잠입해 중앙부로 들어가서 인버전기계와 맞닥뜰일 때, 그 기계에서 두명의 무장한 사람이 튀어나온것은 완전히 시간역행에서만 보여줄 수 있는 개념이다. 이때 주동자와 다른 무장한 사람이 몸싸움을 하는데 이 때 무장한사람은 인버전이 된 상태인데, 이때 나는 무장한 사람은 '인버전된 물체를 쉽게 다룰 수 있는 훈련된 사람'이라고 깨닫고 인버전된 방어구와 인버전된 총을 자유자재로 쓰는것 같았고, 주동자는 인버전된 물체를 쉽게 다뤄야하는 깨달음을 얻는 과정이라고 생각했지만 전혀 아니었다. 싸움의 마지막에 무장한 사람은 비행기엔진의 바람에 빨려들어 폭발과 함께 죽은줄 알았는데 아니었다! 여기까지 놀란의 계획같다.

 

그 후, 사토르와 주동자,닐의 자동차 추격전에서 거꾸로 달리는 차가 있는데 나는 후진을 잘하는! 훈련된 사토르의 부하인줄 알았는데 어째 후진이 너무 빠르다... 추격전이 끝나고 사토르에게 잡힌 주동자와 캣은 어느 두개로 나뉘어진 방으로 들어가서 처음으로 인버전되는 과정을 보게 된다. 이 후에 본격적인 스토리가 진행되면서 계속 인버전이 나오기 때문에 헷갈려하는 상황이 나온다. 그러나 100% 이해하지 않아도 된다. 큰 흐름만 보면 된다.

 

자동자 추격전에서 주동자는 인버전이 되었으니 추격전 방향으로 시간이 흐르기 때문에 아까의 추격전이 또 일어나는데 후진하는 자동차는 주동자 자신이었다. 여기서 우리는 재밌는 점을 발견할 수 있는데, 주동자시점에서 첫번째 추격전이 일어날 때 미래에서온 주동자와 같이 있었는데 자신이 인버전되서 두번째 추격전에서 주동자의 행동은 첫번째 추격전에서본 미래의 주동자와 같은 행동과 결과를 똑같이 따라한다.

 

첫번째 추격전에서 주동자는 미래의 주동자를봐서 '결과'를 미리 봤고, 두번째 추격전에서 주동자 스스로의 행동에 '원인'이 생긴다. 테넷 초반에서 주동자와 연구원이 인버전된 총알을 가지고 대화를 나누는데 그때 원인과 결과에 대해서 얘기가 나오고 주동자는 결과가 먼저 나오고 원인이 그 뒤에 나온다는것을 이해하지 못하지만 그 주동자도 결국 결과-원인순서대로 행동을 하게 된다. 이것은 원인-결과, 결과-원인에 대해 이해하지 않더라도 무언가가 그를 그렇게 만들게 된다는 것을 뜻한다.

이것은 영화 초반부에 주동자가 말하는 자유의지 그리고 결정론과 관련된 이야기 인데 인간은 버튼을 누르려면

'누른다는 생각->누르려는 근육의 움직임->누름'이라는 과정을 거치게 되는데 실제로 실험을 해보니

'누르려는 근육의 움직임->누른다는 생각->누름'처럼 근육이 먼저 움직이고 생각이 그 뒤에 나타나게 된다. 인간은 우리의 의지로 결정하게 되는 줄 알았던 상식이 깨지게 된 것인데, 그말은 다시 말하면 결과가 먼저 나타나고 그 뒤에 원인이 나타나도 문제는 없다는 뜻이다. '일어날 일은 일어난다'라는 테넷의 대사의 의미는 이것을 말한다고 본다.

 

자동차 추격전이 끝나고 주동자와 캣이 끌려간 장면에서 아이브스(테넷팀)가 나타나 주동자를 사토르에게서 구해주는데, 여기서 많은 정보가 나온다.

1. 우선 사토르와 주동자가 대화할 때 사토르의 말이 한박자 늦게 스피커를 통해 나온다. 이건 인버전된 사토르의 말은 거꾸로 나오므로 이걸 다시 거꾸로 바꾸는 딜레이인듯 하다.

2. 아이브스는 인버전기계에 대해 훨씬 해박한 지식(기계 사용량, 반대편방관측 등)을 보여주는데 여기서 아이브스와 닐은 생각보다 더 먼 미래에서 왔다는 것을 암시한다. 

3. 아이브스는 반대편방에 우리의 모습이 나오지 않는다면 인버전기계에 들어갈 수 없다고 하는데 내 생각엔 인버전된 사람이 과거의 결과를 바꿔버려 패러독스를 일으키면 모습이 나오지 않는것 같다.

3번은 마치 양자얽힘같다. 양자 얽힘은 두 입자가 아무리 멀리 떨어져 있어도 A입자의 상태가 변하면 그즉시 B입자의 상태에 영향을 주는데 이것은 마치 A입자의상태변경에 대한 정보가 광속으로 B입자에게 전달한것으로 보여지지만, 광속보다 멀리 있어도 입자는 즉시 변경되기 때문에 광속이라는 기존의 물리학으로 생각하면 안된다. 양자역학에서 입자를 관측하는 순간 입자의 상태는 변하게 된다. 그러니 관측만으로도 결과가 바뀌게 됨.

A입자관측->A입자상태변경->B입자에게 즉시영향.

반대편방관측->인버전으로 상태변경->결과에게 즉시영향->결과가 패러독스라면?-> 역설이기 때문에 관측조차 되지 않음.

놀란이 여기까지 생각한지는 모르겠지만 재밌는 추측이다.

 

두번째 자동차 추격전까지 끝나면 다시 프리포트 추격전으로 돌아가서 두번째 프리포트 추격전이 시작된다. 여기서 나는 충격을 받고 말았는데 첫번째 프리포트 추격전에서는 두명의 무장된 사람이 인버전기계에서 튀어나왔는데 인버전주동자의 시점에서 보니까 모든게 주동자 한명의 행동이었다는 것이 재밌는 연출이었다. 여기서 개소름 ㄷㄷ

 

다음은 스탈스크 전투인데 인버전이 아닌사람과 인버전이 된사람 두 팀이 동시에 투입되서 일어나는 전투는 흥미로운 연출이다. 여기서 시간방향이 좀 헷갈리는데 굳이 스토리 놓쳐가며 이해하려하지 않아도 된다. 그런 이해는 나중에 유튜브를 봐서 이해해도 되고 중요한건 이해하기 위해 재미를 놓치지마라! 그냥 흘러가는대로 보면 된다.

 

-----------------------------------------------------------------------------------------------------------

 

인터스텔라는 우주, 블랙홀, 고차원의 영상미와 그걸 합리적으로 관객을 설득하는게 재미였다면

테넷은 과학적상황을 부여한 첩보영화라고 봐야한다. 만약 인터스텔라처럼 관객을 설득하려면 인버전 기계가 어떻게 1회밖에 못쓴다는 구체적인 수치가 나왔는지, 인버전 기계의 작동원리, 반대편방에 본인의 모습이 나오지 않았을 때 인버전 기계에 들어가면 어떤 일이 일어나는지, 미래와 과거의 사람이 부딪혔을 때, 할아버지 역설, 열역학 등 더 자세한 설명이 나왔을거라고 본다.

그러나 놀란은 구체적은 설명은 하지 않는다. '엔트로피를 거꾸로 돌리는 기계'가 있다면...이라는 상상으로 만들어낸 영화일 뿐이다.  그 상상을 설득시키기 위한 최소한의 장치만 있다. 놀란은 그렇게해서 관객이 자연스럽게 테넷이란 영화를 과학잡지같은 영화가 아니라 스토리와 연출에 집중할 수 있게 한다. 그리고 당연히 구체적인 설명이 없더라도 관객은 영화를 재밌게 볼 수 있다!

 

관객이 완벽히 영화를 이해하지 못했더라도 감독이 주고싶은 재미를 느꼈다면 그건 잘만든 영화라고 생각한다.

 

유튜브에 테넷 과학에 대한 영상이 많고 이것을 봐야만 테넷을 이해할 수 있다고 보는 사람이 있는데 그것은 그냥 테넷DLC의 느낌이다. 일단 테넷을 봐서 '영화의 재미'라는 것에 집중하고 나중에 유튜브로 테넷 과학을 보면 된다.

'영화 감상평' 카테고리의 다른 글

파묘 감상평 스포있음  (0) 2024.09.07

3일내내 개지랄떨면서 우회로 해결한 이슈이다...

 

현재 프로젝트에서 데이터테이블을 20개가량 사용중이고

게임인스턴스::생성자에서 FObjectFinder로 데이터테이블을 불러와서 사용중인 상황이다.

근데 패키징후 프로젝트를 키면 검은화면만 나오고 1~2초후 응답없음으로 진행이 되지 않는다.

이것은 Shipping빌드에서도 마찬가지였다...

 

로그찍어가면서 별별 지랄한 끝에 문제의 지점을 발견했는데

특정 데이터테이블을 FObjectFinder로 불러오는 순간 프로젝트가 멈춰버린다.

FObjectFinder로 불러올 때 성공,실패여부를 확인할 수 있지만 그런 문제가 아니라 그냥 멈춰버린다.

특정 데이터테이블의 정체는 영웅(캐릭터)의 데이터를 모아둔 테이블이였다.

 

영웅 데이터테이블의 변수중 하나는 TSubclassOf<>를 사용해서 영웅클래스를 지정하는 변수를 사용중인데

나는 c++영웅클래스가 아니라 c++에서 상속받은 BP영웅클래스를 데이터테이블에 넣어서 사용중이였다.

테스트를 포함해서 여러모로 영웅클래스는 블루프린트가 굉장히 편하기 때문에 애용중이였는데 혹시 특정 영웅의 블루프린트가 문제가 아닐까 싶어서 데이터테이블의 모든 값(행)의 TSubclassOf<>를 None으로 없애버리니까 게임이 시작된다!!

그래서 특정 영웅 블루프린트가 문제인가 싶어서 계속 디버깅해봤지만 뭔가이상하다 어쩔땐되고 어쩔땐 안되고 지 맘대로 된다. 점점 더 미궁으로 빠져들어가는데 몇시간 디버깅후 또 하나 알아냈다.

1. TSubclassOf<>가 문제인건 맞음

2. 10개행,20개행...그러니까 데이터테이블의 행 개수와 상관없이

TSubclassOf<>변수에 영웅블루프린트 클래스를 각기다른 4개이상의 클래스를 넣으면 문제가 생김.

간단하게 설명하기가 어렵다.. 10개행에 영웅_BP_A클래스로 꽉 채우면 문제가 없지만,

아무 행에 영웅_BP_A, B, C, D의 각기다른 블루프린트 클래스가 4개이상 행에 들어가 있다면 문제발생.

순서상관없고 행위치도 상관없다...

3. 영웅블루프린트가 아니라 영웅C++클래스는 또 문제없음.

4. 영웅클래스가 아니라 AActor를 상속받은 블루프린트도 문제없음.

너무 얼탱이가 없다. 안되면 그냥 안될것이지 왜 4개이상의 각기다른 클래스가 데이터테이블에 들어가면 문제가 생기고, 또 같은클래스를 넣으면 문제없고...

이 지점에서 그냥 영웅클래스를 C++로만 사용할까...도 생각했지만 BP를 사용하는것이 생산성,테스트등 훨~씬 편하기 때문에 BP를 포기할순 없었다.

 

아~혹시 블루프린트클래스가 포함된 데이터테이블이 초기화되는것보다 게임인스턴스생성자가 더 빨리 실행되서 초기화 되지 않는 데이터테이블을 불러오기 때문에 문제가 생긴것일까? 싶었다. 실제로 8개행밖에 없는 영웅데이터테이블을 에디터에서 로드하는데 약간의 딜레이가 생긴다. 임포트시 5~10초정도 걸린다. 영웅 데이터테이블만 생성자에서 불러오지 않고 게임인스턴스::Init() override 함수에서 LoadObject()로 런타임중 데이터테이블을 가져오는 시도를 해보았지만 되지 않았다.

 

이때, 번뜩인 생각이 혹시 영웅 데이터테이블이 패키징시 아예 누락된 것이 아닐까? 싶어서

프로젝트설정->패키징->Pak압축(Pak 파일 사용)->false로 설정해서 패키징된 에셋을 직접보니 역시 영웅데이터테이블만 누락되어 있었다. 미리 말씀드리면 아직도 왜 누락되었는지 모르겠다. 그래서 패키징할때 여러 방법으로 영웅데이터테이블을 강제로 패키징하는 방법을 찾아봤다.

1. 프로젝트설정->패키징->패키징할 추가 비에셋 디렉토리에 경로추가

-> 실패

2. 프로젝트설정->패키징->복사할 추가 비에셋 디렉토리에 경로추가

-> 실패

3. 프로젝트설정->패키징->쿠킹할 추가 에셋 디렉토리에 경로추가

 -> 성공했으나 너무 Raw한 모양의 데이터테이블에셋이 패키징경로에 추가되어있어서 상당히 찝찝했다.

그리고 경로를 추가하는방법이라 같은 폴더에 있는 모든 데이터테이블이 Pak압축되지 않고 파일명이 그대로 노출되기 때문에 영웅데이터테이블만 따로 다른폴더에서 관리해야하는데 여러모로 참 귀찮다. 결국 이 방법은 사용하지 않는다.

 

이렇게 테스트를 해보니 패키징용량이 왔다갔다하는것이 보였다.

Pak압축기준 약2기가정도 되는 패키징파일이 영웅데이터테이블을 불러오지 않으면 450메가정도로 확 줄어든다. 아마 영웅데이터테이블에 참조된 영웅클래스와 관련된 메쉬, 머터리얼등의 에셋이 빠져서 그런듯하다.

이때 과거 패키징관련 글을 읽은 기억이 났다.

 

패키징은 프로젝트에 참조된 에셋만 패키징이 되고, 프로젝트에 에셋이 있더라도 어디서도 참조형식으로 사용되지 않으면 패키징에 추가되지 않는다.

 

이 생각이 나면서 혹시 c++코드로만 참조하고 있던 데이터테이블을 블루프린트에 변수로 넣으면 더 확실하게 참조(진짜 더 확실한건진 모름)가 되고, 그러면 어떤식으로든 패키징시 데이터테이블이 패키징에 누락되지 않고 제대로 되지 않을까?라는 생각이 들면서 곧바로 시도했다.

게임인스턴스C++에

UPROPERTY(EditAnywhere)

UDataTable* _hero_datatable = nullptr;

이런식으로 변수를 하나 만들고 게임인스턴스BP에서 _hero_datatable에 영웅 데이터테이블을 넣고 패키징을 해봤다.

 

드디어... 패키징에 영웅 데이터테이블이 누락되지않고 잘 들어가있었다. 감격의 순간이었다.

사소한 단점은 이상하게 생성자에서는 여전히 불러오진 못한다...

분명 Pak압축을 하지 않은 패키징파일에서 분명! 영웅 데이터테이블에셋이 있는걸 두눈으로 봤는데 생성자에서 불러오진 못한다. 그래서 _hero_datatable변수를 직접 사용하는데 아직까지 문제는 없다.

#if WITH_EDITOR 를 이용해서 에디터에서는 생성자에서 불러오고, 에디터가 아니라면 _hero_datatable을 사용하는 방법을 채택했다. 

 

아직도 왜 패키징에서 누락되는지 모르겠다. 열심히 구글링해봤지만 동일한 문제를 겪는 글은 찾지못했고 비슷한 글을봐도 유의미한 내용은 없었다. 

 

혹시 같은 문제를 겪는다면 이런방법으로 우회로 해결하는 방법으로 도움이 되셨으면 좋겠다.

 

------------------------------------------------------------------------------------------------------------------------------------------------

TSubclassOf<>를 TSoftClassPtr<>로 변경하니까 잘된다...

결국 우회해서 해결한 방법은 없애고 TSoftClassPtr<>를 사용해서 정상?적으로 데이터테이블을 불러오고있다

 

암만 구글링해도 TSoftClassPtr<>와 패키징관련한 글이 안보여서 정확한원인은 찾지못한 상태...

	//가장 앞, 뒤의 데이터를 삭제합니다
	const bool RemoveFront()
	{
		return Remove(head->next);
	}
	const bool RemoveBack()
	{
		return Remove(tail->prev);
	}

    private:
	//데이터를 찾아서 삭제합니다
	const bool Remove(Node* nd_remove)
	{
		if (size <= 0) return false;

		nd_remove->prev->next = nd_remove->next;
		nd_remove->next->prev = nd_remove->prev;
		delete nd_remove;
		--size;
		return true;
	}

이전 포스팅에서 소개한 AddFront(),AddBack(),Add()와 동일한 구조의 함수다

Remove()는 성공과 실패를 리턴하고, 노드를 삭제하며 삭제된 노드 앞뒤의 노드끼리 연결시킨다

 


	//원하는 데이터를 찾아서 삭제합니다
	const bool RemoveData(const Data& data)
	{
		if (size <= 0) return false;

		for (Node* nd = head->next; nd != tail; nd = nd->next)
		{
			if (nd->data == data)
			{
				//삭제할 데이터를 찾았습니다. 삭제합니다
				return Remove(nd);
			}
		}

		//삭제할 데이터를 찾지 못했습니다
		return false;
	}

	//모든 데이터를 삭제합니다
	void RemoveAll()
	{
		while (RemoveFront()) {}
	}

 

 

RemoveData(): 동일한 데이터를 찾아 삭제.

RemoveAll(): 모든 데이터를 삭제.

 

RemoveData()에서 노드의 순회을 볼 수 있다.

head->next노드부터 시작해서 tail이 아닌 노드면 계속 순회한다.

 

역순회도 다르지 않다.

for (Node* nd = tail->prev; nd != head; nd = nd->prev)

논리적으로 거꾸로 돌아가게 하면된다.

 


프로그래밍 처음 공부했을때 꽤 어려웠던 연결리스트였는데 지금 다시 보니까 구현자체는 어렵지 않았다.

데이터의 삽입,삭제시 노드연결만 신경써주면된다.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

연결리스트는 여러개의 노드로 구성되어 있기 때문에 노드 구조체를 먼저 만들어보자

	//template <typename Data>
    struct Node
	{
		Data data = NULL;
		Node* next = nullptr;
		Node* prev = nullptr;
	};

노드는 대단한건 없고, 데이터를 저장하기위한 변수와, 앞 뒤 노드를 가리키는 변수2개가 필요하다.

처음 프로그래밍을 공부할 때 이해가 어려웠던 부분이

 

Node구조체안에 왜 똑같은 노드가 2개가있지? 그리고 왜 포인터지?

노드는 노드를 가리켜야 하니까 저렇게 한건 알겠는데 논리적으로 이해를 못하니까 두루뭉술하게 이해했고,

왜 포인터로 저장하는지도 이해를 못했다.

 

노드 구조체안에 노드변수를 가져야하는것은 구현하면 알게되니 넘어가고

포인터로 저장하는 이유는 노드를 '복사'해서 가지지 않으려고 그런것이다.

call by value, call by address, call by reference라는 개념이 있는데 

여기서 이것까지 설명하면 길어지기 때문에 이 부분은 넘어가고 여기선 일단

노드구조체안의 노드변수는 앞 뒤 노드의 정보를 원본으로 가지고 있어야하기 때문이라고 이해하자


	MyLinkedList()
	{
		head = new Node;
		tail = new Node;

		head->next = tail;
		tail->prev = head;
	}
	~MyLinkedList()
	{
		//모든 노드 삭제
		RemoveAll();

		delete head;
		delete tail;
	}

	Node* head = nullptr;
	Node* tail = nullptr;
	size_t size = 0;

생성자에선 헤드와 테일을 미리 생성하고 서로 연결시켜 놓습니다.

소멸자에선 모든 데이터를 제거및해제 하고, 헤드와 테일도 해제합니다. 항상 포인터같은 할당된 변수는 꼭 해제를 해야합니다.

RemoveAll()은 다음 포스팅에서 나옵니다

 

내가 구현한 이중연결리스트는 헤드와 테일에 데이터가 없습니다


	//데이터를 추가합니다
	void AddFront(const Data& data)
	{
		Add(head->next, data);
	}
	void AddBack(const Data& data)
	{
		Add(tail, data);
	}
    
    private:
	void Add(Node* nd_next, const Data& data)
	{
		//노드를 새로생성합니다
		Node* nd = new Node;
		nd->data = data;

		//생성한 노드의 앞, 뒤 노드를 연결합니다.
		nd->next = nd_next;
		nd->prev = nd_next->prev;

		//앞, 뒤 노드도 생성한 노드에 연결합니다
		nd_next->prev->next = nd;
		nd_next->prev = nd;

		++size;
	}

AddFront(), AddBack()은 앞 혹은 뒤에 데이터를 추가하겠는 함수고 실제 로직은 Add()에 들어있다.

Add()를 보면 생성된 노드와 앞,뒤의 노드를 연결하는 부분이 헷갈릴수 있다.

하지만 하나씩 하나씩 나눠서 보면 이해가 될것이다.

그래도 이해가 안되면 직접 그려보자.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

연결리스트는 데이터끼리 순서대로 연결된 형식이다.

벡터(vector)의 경우 메모리의 인접한 순서대로 데이터가 존재하지만,

연결리스트는 각 데이터가 메모리에서 인접하지 않더라도 데이터는 다음 데이터의 위치를 저장하고 있다.

 

벡터는 데이터를 중간위치에서 삽입,삭제시 데이터가 밀리거나, 땡겨지게 되는 이슈가 있는데,

연결리스트는 메모리가 인접하지 않아도 되기 때문에 삽입,삭제에서 자유롭다.

단점은 벡터는 인덱스로 데이터에 빠르게 접근할 수 있지만, 연결리스트는 꼭! 순차적으로 검색후 데이터에 접근하게 된다. 그래서 검색의 최후방에 있는 데이터는 가장늦게 찾게 된다.

 

벡터는 중간위치에 삽입,삭제 하려면 인덱스를 알아야 하지만,

연결리스트는 앞 혹은 뒤의 노드를 알아야한다.

 

여기선 어떻게 데이터끼리 위치를 저장하고있는지 알아봐야한다


 

1. 단일연결리스트

데이터는 다음데이터의 위치를 저장한다

각 네모아이콘은 데이터를 입력할 수 있는 노드(Node)를 의미하고, 그중 가장 앞에있는 노드를 Head 또는 Header라고 한다.

나는 헤드(Head)에 데이터를 넣지않고 빈 공간으로 표현했지만, 구현에 따라서 헤드에도 데이터를 넣어도 된다. 어차피 헤드도 노드의 종류여서 문제없다.

화살표를 보면 각 노드는 다음 노드를 가리키고 있고, 마지막 노드는 다음 노드가 없기 때문에 아무것도 가리키지 않는다.

단일연결리스트의 이름에서 볼 수 있듯이 한쪽방향으로만 데이터가 연결되어 있다 

3번 노드의 데이터를 가져오려면 헤드 혹은 1번 노드부터 순차적으로 검색한다.

 

1번 노드야 너 3번이니? 아니

2번 노드야 너 3번이니? 아니

3번 노드야 너 3번이니? 응 

...

 

마지막 위치에 데이터를 추가하려면 마지막 노드를 찾아야하는 긴 여정이 필요하다.

그렇기 때문에 맨 앞에 데이터를 추가하는것이 여러모로 이롭다.

 

이미지가 선형적으로 되어 있어 마치 메모리에서도 인접해 보이고 정렬된 느낌이지만 이렇게 표현할 수도 있다

 

노드의 위치는 신경쓰지 않아도 된다.

그저 노드와 노드끼리의 연결만 신경쓰자.


2번 노드를 삭제해보자

벡터와 다르게 중간의 데이터를 삭제해도 각 데이터(노드)의 위치는 변하지 않는다

다만 2번 노드를 가리킨 1번 노드는 이제 2번이 아니라 2번의 다음 노드인 3번 노드를 가리키게 된다.

어렵지 않다. 그냥 가리키는것만 신경쓰자!!

 


2. 원형연결리스트

마지막 노드는 다시 첫 데이터(노드)를 가리킨다

단일연결리스트는 빈 헤드가 없는것이 기본적이다.

 

단일연결리스트에서 딱 하나 추가된 기능이 있는데

마지막노드가 빈 공간을 가리키지않고 첫 데이터를 가리킨다.

 

나는 원형연결리스트를 써본적이 없어서 그런지... 단일연결리스트보다 좋은 점을 잘 모르겠다.

굳이 따지면 마지막 노드가 nullptr을 가리키지않아 크래쉬가 날 확률이 적어지지만,

무한루프에 빠질수도 있는 단점도 가지고 있다


3. 이중연결리스트

모든 노드가 앞뒤로 연결되어 있어 역순환을 할 수 있다.

헤드와 동일한 개념의 테일(Tail)노드가 새로 생긴다.

단일, 원형연결리스트의 단점인 마지막노드검색이 훨씬 수월해진다.

 

단점은 이전 노드를 기억해야하는 변수를 만들고, 테일노드까지 생성해야해서 메모리를 좀 더 쓰게되지만, 

단점에 비해 장점이 크다.


나는 이중연결리스트만 구현했고 단일,원형연결리스트는 따로 구현하지 않았기 때문에

이중연결리스트만 소개하겠다.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

구글링으로 원형큐 구현을 보면 이상하게 원형큐의 사이즈가 정해져있고, 꽉차면 원형큐의 크기가 커지지않고 채울수 없는 구현만 봤다. 내가 못본걸수도 있지만,,,

이건 너무 이상해서 언제든 크기조절이 가능한 큐를 구현해보았다.

 

기존 원형큐의 단점은 위에 설명했듯이 큐에 데이터가 꽉차면 더이상 값을 넣을 수 없고, 그렇다고 큐의 사이즈를 키울 수도 없고, 빈공간을 효율적으로 관리하지만, 큐의 사이즈를 초기값설정후 다시 늘릴 수 없는 것이다.

 

분명 개발중 큐의 크기를 추정하기 애매할 때가 있을 거라 생각하고 원형큐의 장점인 빈 공간활용을 살리되, 언제든 벡터(vector)처럼 큐의 크기를 늘릴수 있는 기능을 추가하고 싶었다.


	void Reserve(size_t capacity) override
	{
		//기존 캐퍼시티가 더 크다면 재할당을 하지 않습니다
		if (this->my_capacity >= capacity)
			return;

		Data* temp = new Data[capacity];

		//Debug
		for (size_t i = 0; i < capacity; ++i)
		{
			temp[i] = NULL;
		}

		//0번째 인덱스부터 rear까지 새로 할당한 배열에 복사합니다
		for (size_t i = 0; i < _rear; ++i)
		{
			temp[i] = this->my_base[i];
		}

		//컨테이너에 원소가 있다면 새로 할당한 주소로 옮겨줍니다
		if (this->my_capacity > 0 &&
			this->my_size > 0 &&
			_rear <= _front)
		{
			//front부터 마지막인덱스까지 새로 할당한 배열에 복사합니다
			for (size_t i = 0; i < this->my_capacity - _front; ++i)
			{
				temp[capacity - i - 1] = this->my_base[this->my_capacity - i - 1];
			}
			_front += capacity - this->my_capacity;

			delete[] this->my_base;
		}

		
		this->my_capacity = capacity;
		this->my_base = temp;
	}

	//마지막 위치에 원소를 추가합니다
	//Enqueue()
	void PushBack(const Data& data)
	{
		//rear의 위치가 배열의 최대 크기를 넘었는지 확인
		if (_rear >= this->my_capacity)
		{
			_rear = 0;
		}

		//컨테이너에 비어있는 자리가 있는지 확인
		//rear와 front가 동일한 인덱스를 가리키면 큐가 꽉찼거나 비어있음을 의미합니다
		if (_rear == _front)
		{
			//큐가 꽉찼으면 재할당합니다
			if (this->my_size > 0)
			{
				this->Reserve(this->my_capacity + this->additive_capacity);
			}
		}

		//추가합니다
		this->my_base[_rear++] = data;
		++this->my_size;
	}

	//가장 처음으로 들어온 원소를 반환합니다
	//디버깅을 위해 bool로 반환의 실패여부를 체크합니다
	//Dequeue
	const bool PopFront(Data& out_data)
	{
		//rear와 front가 동일한 인덱스를 가리키면 큐가 꽉찼거나 비어있음을 의미합니다
		if (_rear == _front)
		{
			//큐가 비어있으면 Pop을 하지 않습니다
			if (this->my_size <= 0)
			{
				return false;
			}
		}

		if (_front >= this->my_capacity)
		{
			_front = 0;
		}

		--this->my_size;

		//return this->my_base[_front++];

		//Debug
		Data& data = this->my_base[_front++];
		Data data_new = data;
		out_data = data_new;
		data = NULL;

		return true;
	}

PushBack()을 보면 데이터가 꽉 찰때Reserve()함수를 사용하는데 이것말고는 기존 원형큐과 똑같고 PopFront()도 변경점은 없다.

 

자세히 봐야할것은 Reserve()함수인데 이 함수는 이전에 벡터(vector)구현할 때 소개했던것과 동일하지만, 기존 데이터를 재할당한 배열에 추가하는 방법이 조금 달라졌다.

 

데이터가 앞쪽에 그대로 있다면 이런식으로 재할당한 배열에 추가된다

벡터와 동일하게 데이터가 추가되지만

만약 뒷 공간이 꽉차서 앞 공간까지 넘어오게 되었을때는 이런식으로 동작한다

이건 벡터와 동일하다.

 

rear가 뒷공간을 넘어서 앞으로 넘어오게된 상황에서 어떻게 재할당이 이뤄지는것이 핵심이다

rear와 front사이의 공간을 확보한다.

이말은 추가데이터의 위치를 가리키는 rear의 뒷부분을 확보한다고 봐도 된다

그러기 위해 front를 기준으로 뒷부분의 데이터를 옮긴다.

 

데이터가 꽉차서 자동으로 재할당이 될 경우는 이렇게 된다.

rear와 front가 겹치고, 데이터가 있다는 뜻은 꽉찼다는 뜻이므로 rear와 front가 겹쳐있을 때 재할당이 이루어진다.

front위치 이후의 3개의 데이터가 계속 뒤로 밀리고 있으며 rear위치뒤에 빈공간을 계속 생성하는게 보일것이다.

 

벡터와 마찬가지로 크기재할당은 가급적 피하는것이 좋다. 

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

원형큐의 구현은 어렵지 않다.

Enqueue하는데 뒷 공간이 없다면 앞으로 오면되고,,,

Dequeue또한 마찬가지이고...

 

그래서 딱 하나만 추가하면 된다

큐가 꽉찼거나 비어있음을 판단

이것만 구현한다면 원형큐도 이젠 쉽다!

 

	MyQueue_FixedCircle(size_t i_init_size)
	{
		//my_base가 nullptr지만
		//사이즈및 캐퍼시티가 0이므로 원소를 추가할때 Reserve()함수에서 할당이 이뤄집니다
		this->my_size = 0;
		this->my_capacity = i_init_size;

		Data* temp = new Data[this->my_capacity];

		this->my_base = temp;

		//Debug
		for (size_t i = 0; i < this->my_capacity; ++i)
			this->my_base[i] = NULL;
	}
    //마지막 위치에 원소를 추가합니다
	//Enqueue()
	const bool PushBack(const Data& data)
	{
		//rear의 위치가 배열의 최대 크기를 넘었는지 확인
		if (_rear >= this->my_capacity)
		{
			_rear = 0;
		}

		//컨테이너에 비어있는 자리가 있는지 확인
		//rear와 front가 동일한 인덱스를 가리키면 큐가 꽉찼거나 비어있음을 의미합니다
		if (_rear == _front)
		{
			//큐가 꽉찼으면 데이터를 채울 수 없습니다
			if (this->my_size > 0)
			{
				return false;
			}
		}

		//추가합니다
		this->my_base[_rear++] = data;
		++this->my_size;
		return true;
	}

	//가장 처음으로 들어온 원소를 반환합니다
	//디버깅을 위해 bool로 반환의 실패여부를 체크합니다
	//Dequeue
	const bool PopFront(Data& out_data)
	{
		//rear와 front가 동일한 인덱스를 가리키면 큐가 꽉찼거나 비어있음을 의미합니다
		if (_rear == _front)
		{
			//큐가 비어있으면 Pop을 하지 않습니다
			if (this->my_size <= 0)
			{
				return false;
			}
		}

		if (_front >= this->my_capacity)
		{
			_front = 0;
		}

		--this->my_size;

		//return this->my_base[_front++];

		//Debug
		Data& data = this->my_base[_front++];
		Data data_new = data;
		out_data = data_new;
		data = NULL;

		return true;
	}

다른 원형큐 구현을 보면 %연산자를 쓰고, isEmpty() isFull()등의 함수를 추가로 구현하던데 나는 그렇게 하지않고 최대한 직관적으로 구현했다.

EnQ나 DeQ를 하면 rear, front를 ++해주고 최대 사이즈를 넘어가면 0으로 돌아가게 하고,

내 원형큐는 데이터의 개수를 카운팅하고 있어서 rear==front라면 데이터개수를 판단해 큐가 비었는지 꽉찼는지 알 수 있다.

 

생성자를 보면 원형큐의 크기를 '무조건' 설정해줘야한다. 중간에 늘리거나 줄일 수 없다.

다른 원형큐 구현을 보면 const size_t QUEUE_SIZE = 1000; 뭐 이런식으로 하드코딩 해놓기도 하는데 똑같으면 재미없으니까 사용자가 원하는대로 크기를 조절할 수 있게 해놓았다

 

생성자 코드의 자세한 내용은 여기서 Reserve()구현부를 보면 확인할 수 있다.

https://forestbird0.tistory.com/38

 

[C++] 나만의 Vector만들기 (2) - 생성자, 소멸자, 캐퍼시티, Reserve

생성자와 소멸자 코드 MyVector() { //my_base가 nullptr지만 //사이즈및 캐퍼시티가 0이므로 원소를 추가할때 Reserve()함수에서 할당이 이뤄집니다 this->my_base = nullptr; this->my_capacity = this->my_size = 0; } MyVecto

forestbird0.tistory.com

 

다음 포스팅은 여기서 더 발전된 '동적 원형 큐'인데 대단한건 아니고 사이즈를 언제든 늘릴 수 있게 구현한 원형큐다.

 

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

이전 포스팅에서 구현했던 '동적 배열 큐'(선형큐)의 단점을 보완한 '원형 큐'.

원형큐는 선형큐의 단점인 '빈 공간 낭비'를 해결해서, 비교적 효율적으로 빈 공간을 관리한다.

원형큐를 간단하게 설명하면 데이터를 추가할 때 뒷 공간이 꽉차면 다시 비어있는 앞공간을 메꾸는 원리이다.

 

선형큐처럼 front와 rear변수로 데이터의 입출력위치를 관리한다

front: Dequeue()호출 시 꺼낼 데이터의 위치를 가리킴.(큐에 데이터가 없다면 빈 공간을 가리킴.)

rear: Enqueue()호출 시 데이터를 추가할 빈 공간을 가리킴.(큐에 데이터가 꽉차면 빈 공간이 아니라 데이터를 가리킴)

나는 front는 꺼내야할 데이터의 위치를, rear는 데이터가 추가될 빈공간의 위치를 가리키게 했는데,

구현하는 사람에 따라서 다르게 작성할 수도있다. 

 

여기서 데이터를 En,Dequeue하게 되면...

rear는 항상 빈 공간을 가리켜야하고, front는 항상 데이터를 가리켜야한다.

rear가 데이터를, front가 빈공간을 가리키는것은 큐에 데이터가 꽉차있거나 비었다는 뜻이다.

 

여기서 데이터를 3개이상 추가한다고 했을 때, 선형큐같은 경우 큐의 크기가 커져야 한다.

원형큐라면?

숫자 7데이터가 큐의 처음 위치로 오게 되었다.

이 모양을 선형으로 표현하면 아래의 모습으로 나온다

 

여기서 데이터를 하나 더 추가하면?

이런 모습이 되고 front와 rear가 같은곳을 가리키고 있다.

이렇게 front와 rear가 같은곳을 가리킨다면 큐가 비어있거나, 꽉찼다는 의미이다

 

꽉 차게 되면 Enqueue를 할 수 없다.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

동적 배열로 구현하면 큐의 단점을 명확하게 볼 수 있다.

구현은 쉽지만, 직접 구현해보면서 어떤 문제가 있는지 보자

이렇게 데이터를 추가하면 차곡차곡 쌓이는데 데이터를 꺼내면 앞 공간이 비게 되고 이 빈 공간은 낭비가 된다.

이걸 방지하기 위해 이런 방법도 있는데...

데이터를 꺼낼 때 마다 나머지 데이터를 한칸씩 앞으로 옮기는 방법이 있다.

하지만 딱 봐도 성능에 좋아 보이지 않는데, 데이터가 수천 수만개라면 이런 방법은 사용하기가 어렵다.

 

구현은 어렵지 않으니 일단 구현해보고, 다음 포스팅엔 이런 문제를 개선한 '원형 큐'를 소개하겠다

 


template <typename Data>
class Container_Master
{
protected:
	Data* my_base = nullptr;
	size_t my_capacity = 0;
	size_t my_size = 0;

	//캐퍼시티를 늘릴 때 지정된 값이 없다면 해당 값만큼 늘립니다
	const size_t additive_capacity = 4;
public:
	//컨테이너의 원소개수입니다
	size_t Num()
	{
		return my_size;
	}

	//컨테이너의 캐퍼시티개수입니다
	size_t Max()
	{
		return my_capacity;
	}
};
#pragma once
#include "Container_Master.h"

/*
* 동적 배열 큐 입니다.
*
* 디버깅을 위해 원소를 Pop하게 되면 그 자리를 NULL로 채웁니다
*/

template <typename Data>
class MyQueue_DynamicArray : public Container_Master<Data>
{

public:
	//마지막 위치에 원소를 추가합니다
	//Enqueue()
	void PushBack(const Data& data)
	{
		//rear의 위치가 배열의 최대 크기를 넘었는지 확인
		if (_rear >= this->my_capacity)
		{
			this->Reserve(this->my_capacity + this->additive_capacity);
		}

		//추가합니다
		this->my_base[_rear++] = data;
		++this->my_size;
	}

	//가장 처음으로 들어온 원소를 반환합니다
	//디버깅을 위해 bool로 반환의 실패여부를 체크합니다
	//Dequeue
	const bool PopFront(Data& out_data)
	{
		//큐가 비어있으면 Pop을 하지 않습니다
		if (this->my_size <= 0)
		{
			return false;
		}

		--this->my_size;

		//return this->my_base[_front++];

		//Debug
		Data& data = this->my_base[_front++];
		Data data_new = data;
		out_data = data_new;
		data = NULL;

		return true;
	}

	//앞쪽에 비어있는 배열자리에 기존 원소들로 채웁니다.
	//재할당하는 방법이 있지만, 여기선 빈 공간을 NULL(0)로 표현했습니다
	void SortQueue()
	{
		//원소를 앞으로 이동시킵니다
		size_t i = 0;
		for (i = _front; i < _rear; ++i)
		{
			this->my_base[i - _front] = this->my_base[i];			
		}

		//이동된 원소를 NULL로 채웁니다
		for (i -= _front; i < _rear; ++i)
		{
			std::cout << std::to_string(i) << std::endl;
			this->my_base[i] = NULL;
		}

		_rear -= _front;
		_front = 0;
	}
private:
	//데이터를 꺼낼 공간을 가리키고 있습니다. 데이터가 없을 수도 있습니다.
	size_t _front = 0;
	//데이터를 넣을 빈 공간을 가리키고 있습니다
	size_t _rear = 0;
};

코드가 좀 긴데 일부러 풀어쓰기도 했고, 디버깅으로 코드가 좀 더 길어졌다.

디버깅용으로 큐의 빈 공간은 0(NULL)으로 채웠으니 그점만 유의하면 된다.

 

핵심코드

1. PushBack()(Enqueue)

2. PopFront()(Dequeue)

3. SortQueue() (함수명을 어떻게 작성할지 몰라서 걍 Sort키워드를 썼다...)

굳이 함수명을 Enqueue, Dequeue를 쓰지 않은 이유는 그냥 직관적으로 함수이름만 보고 어떤역할을 하는지 알 수 있게 작성했다.

 

PushBack()은 크게 설명할게 없으니 넘어가고

PopFront()를 보면 데이터를 복사하고 NULL로 바꾸고 반환하고 번거로운 코드가 있는데 디버깅 코드이니 실제 코드는

위에 주석처리한 return base[_front++]; 이것을 보면 된다.

 

SortQueue()는 앞의 빈공간을 채우는 함수이다.

조금 다른점은 PopFront()할 때마다 한칸씩 옮기는 게 아니라 필요할 때 앞의 빈공간으로 이동시키게 한다.

큐를 써야하는데 자주쓰는것은 아니고 원형 큐까지 구현할 필요가 없다면

동적배열로 대충 만들고 빈공간이 5개일때, 10개일때 등 필요한 시점에서 SortQueue()로 빈 공간을 채우면 된다.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

+ Recent posts