블로그 이미지
지누구루

calendar

1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30

Notice

2020. 9. 21. 18:59 공부

Lua 설치.

Visual Studio Code 연동

 

lua의 데이터 타입과, Redis의 데이터 타입간 매칭이 있음.

 

Redis to Lua conversion table.

  • Redis integer reply -> Lua number
  • Redis bulk reply -> Lua string
  • Redis multi bulk reply -> Lua table (may have other Redis data types nested)
  • Redis status reply -> Lua table with a single ok field containing the status
  • Redis error reply -> Lua table with a single err field containing the error
  • Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type

Lua to Redis conversion table.

  • Lua number -> Redis integer reply (the number is converted into an integer)
  • Lua string -> Redis bulk reply
  • Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any)
  • Lua table with a single ok field -> Redis status reply
  • Lua table with a single err field -> Redis error reply
  • Lua boolean false -> Redis Nil bulk reply.

 

Return 타입도 여기 대응대로 매칭.

lua의 string split은 string.gmatch를 이용해서 regular expression을 이용해서 가능.

 

posted by 지누구루
2015. 2. 25. 15:24 공부

아직 생각이 완전히 정리된 내용은 아닙니다.

 

주된 내용은

'어떤 기능을 확장성 있게 만드려고 할때, 어디에 어떻게 기준을 두어야 할까?'

에 대한 고민입니다.

 

현재 일하고 있는 프로젝트에서,

최근에 만들고 있는 기능을 간략히 설명하고 고민하는 내용에 대해서 적어보겠습니다.

 

최근에 고민에 빠지게 한 내용

(사실 비슷한 고민은 굉장히 많이 하는데 그중 하나입니다)

 

RPG 게임이고

던전맵의 타입이 2가지(n가지라고 해도 좋음) 있습니다.

던전 타입에 따라서

  - 시작 방식

  - 진행 방식

  - 종료 방식

  - 몬스터 킬에 의한 보상

  - 게임 종료에 의한 보상

등등이 전부 다르게 적용될수도, 일부는 기존의 다른 던전 타입의 것을 그대로 사용할수도 있습니다.

 

덧) 물론 기획자가 저렇게 어떤 어떤 방식이 다르게 적용될 수 있다고 알려주지 않습니다.

      저렇게 구분하는건 저같은 경우는 보통은 프로그래머의 몫이었습니다.

 

여튼 새로 들어온 작업 내용이

"A타입의 맵에서는 플레이어가 사망할 경우, 경험치 하락이 있지만(레벨 하락은 X), B타입의 맵에서는 경험치 하락이 없게 해달라"

 

라는 내용이었습니다(아주 당연하지만 갑자기 들어온 작업이었지요).

 

현재는 모든맵에서 공통으로 "사망시 경험치 하락" 이 적용되고 있었기 때문에,

이걸 어떻게 작업을 할까... 하고 생각해 보았습니다.

 

 

업계에 들어온지 얼마 되지 않았던 시절의 저라면 고민하지 않고

if(mapType == A) 경험치하락();

else if(mapType == B) 아무짓도안함();

 

으로 if else 추가로 끝냈습니다.

 

그런데 이게, 이렇게 작업하다 보니, 새로운 맵의 타입이 추가되거나,

플레이어 사망시 페널티를 주는 종류가 늘어나는 경우,

점점 해당 코드는 if else 로 뒤덮히게 되고,

특정 맵에서 플레이어 사망시 실행되는 코드와 아닌코드,

그안에서 다시 조건이 들어가면서

(예를 들면 던전타입 A에서 솔로 플레이 시에는 플레이어 사망시 경험치를 하락시키지 않고,

  도움을 주는 NPC를 소환시켜준다. 같은 룰이 추가된다면?)

그래서 진짜로 실행되는 코드가 어느 코드야!! 알수가 없어!!

 같은 말도 안될거 같은 상황을 맛보게 되더란 말입니다.

 

그래서 현재 사용하는 방법중에 하나는.

모든 맵에서 공통으로 실행되는 플레이어 사망시 처리 함수(or 클래스)를 만들고,

던전 타입에 따라 실행되는 함수(or 클래스)를 매핑 시켜주는 방식입니다.

(실제로 사용하고 있는 코드는 구조가 다르지만 이런 방식이라고 이해해도 될것 같습니다)

 

이 경우, 던전 타입에 따라서 플레이어 사망 처리가 함수(or 클래스)별로 따로 나누어지기 때문에,

특정 던전타입에서 플레이어 사망 처리를 찾고, 확인하고, 작업하는데 큰 어려움이 없습니다.

 

하지만 이런 구조로 만드는 경우, .. 사실 한두 종류의 맵에서만 다르게 처리하면되는데,

모두가 override 하도록 강제되므로, 오히려 같은 코드의 복사가 일어날 가능성도 있습니다.

(물론 공통의 동작을 하는 클래스나 함수를 만들어놓고, default로 그녀석을 연결해두면 해결되긴 합니다)

 

 

그런데 여기서 또 고민이 시작되었습니다.

"사망 처리시 경험치 처리가 '맵'의 종류에 기반한 것이라면

그냥 사망 처리 함수에서

if( currentMap.getDiePenaltyType() == PENALTY_TYPE_EXP_DOWN )

과 같이 처리해야 하는 기능 별로 따로 만들수 있지 않을까?

하는 생각이 든겁니다.

 

 

경험치를 예로 들었지만,

예를 들어, "캐릭터 사망시 필요한 기능"을 기능별로 뺀 다음

1) 경험치 다운 기능

2) 도움을 주는 NPC를 소환하는 기능

3) 캐릭터가 사망한걸 친구들에게 알리는 기능

등등이 모두 조합 or 빼는게 가능하다고 한다면.

 

mapType 이 A인 map에

캐릭터 사망시 동작 기능에는 경험치 하락 기능만 build 해두고

mapType이 B인 map의

캐릭터 사망시 동작 기능에는 경험치 하락 기능을 넣지 않고,

다른 기능을 등록해 두어 자동으로 실행되게 하는 방법.

 

이 됩니다.

 

 

앞에서 설명했던 두 방법은 비슷해 보이지만,

관점에 따라서는 완전히 다르게 볼수도 있는 방법입니다.

이 글의 제목을 "확장의 기준" 이라고 쓴 이유는 이런 부분입니다.

 

1) 맵의 종류에 따라서 "캐릭터 사망시 처리"를 모두 분리 할 것이냐

2) 여기서 한단계가 더 확장 가능한, "캐릭터 사망시 처리를 build" 할 수 있게 할 것이냐

 

라는 부분입니다.

2)번의 기능으로 본다면

"경험치 하락" 이라는 아주 범용적인 기능을 만들어 두고,

경험치 하락이 필요한 모든 곳에서 해당 기능을 build 하여 사용할수 있게 됩니다.

 

 

현재 프로젝트에서는 1)번 형태로 되어 있는데,

점점 기능이 여기저기 붙고, 떨어지고, 제약이 다르다거나,

조건만 다르다거나, 하는 경우가 많아 지면서

2)번 형태의 기능 구현을 생각해보게 되었으나....

 

위에서도 말했다시피,

이런 기능 확장 / 변경이 매우 급박하게 치고 들어오는 형태가 되어 버리면...

이런 구조상의 변경은 손대기 어려워지고..(서비스 중이라면 더더욱)

어떤 부분이 어떻게 확장될지는 프로그래머가 스스로 판단하여 그 분기를 만들어야 하기 때문에,

처음부터 어떤 부분이 어떻게 확장될지를 추론하는것은 매우 어려웠습니다...

 

결론은 기능 확장의 기준을 정하는건 어렵더라....입니다.

......

죄송합니다.

끝.

(급마무리 되는 느낌이 들어더 어쩔수 없습니다...)

 

 

+ 한참 뒤에 다시 읽어 보니, 정말 글이 이상하게 되버렸네요;; 한번에 쓴 글이 아니고, 여러번에 나눠 쓰는 도중에 멘붕이 와서 대충 적어 버린거 같습니다. 쓰레기 같은 글이 되버렸지만, 일단 남겨둬 봅니다 ㅠㅠ

 

posted by 지누구루
2015. 1. 29. 13:53 공부

이번에 쓸 내용은 구지 게임 서버에 국한된 내용은 아니고,

여러 케이스에 적용되는 내용으로, 문장으로 표현하면

 

"수치에 의해서 계산가능한 단계 정보를 따로 저장할 것이냐, 아니며 계산해서 사용할 것이냐"

 

의 문제입니다.

 

(1) 경험치 수치

 

가장 먼저 생각할 수 있는 케이스는 바로 캐릭터의 "경험치"와 "레벨"의 관계입니다.

'총 경험치'를 저장하고, 총 경험치로 부터 레벨을 계산하는 방식이 있을수 있고,

레벨과, 현재 경험치를 저장하는 방식이 있을수 있습니다.

각 경우의 장단점을 간단히 보겠습니다.

 

[1] '총 경험치' 를 저장하고, 총 경험치로부터 레벨을 계산하는 방식

+ 장점

레벨이 따로 저장되지 않기 때문에, 캐릭터 정보의 저장 공간이 줄어듬.

경험치와 레벨이 맞지 않는 현상이 나타나지 않음

(경험치는 10렙 경험치인데 레벨은 9인 경우가 존재할 수 없음)

+ 단점

총 경험치를 저장하는 데이터 타입에 따라 한계값이 존재함(총 경험치가 INT MAX를 넘을수 없다던가)

경험치 테이블이 변경될 경우, 보정이 없으면 레벨 다운이나 레벨업 현상이 일어날 수 있음.

=> 반드시 보정이 필요하다.

경험치 보정이 필요한 경우, 다수의 데이터에 대해 쿼리만으로 해결 하기가 어렵다.

 

[2] '레벨'을 저장하고, 해당 레벨에서의 현재 경험치를 저장하는 방식

+ 장점

경험치 -> 레벨로 변환하는 과정이 없으므로 연산횟수가 약간 적다

DB에서 바로 레벨을 알아 보기 쉽다.

경험치 테이블이 변경될 경우, 레벨은 그대로 유지할 수 있다.

경험치 보정이 필요할 경우, 쿼리만으로 해결할수 있는 가능성이 있다.

+ 단점

경험치 총 획득량과 레벨이 맞지 않는 경우가 발생할 수 있다.

 

인데, 각각의 장단점이 있고, 각 단점을 보완할 수 있는 방법이 있기 때문에,

결국은 선택의 문제가 될 것 같습니다.

그리고 가장 개인적으로 가장 중요하다고 생각하는 점은

 "경험치 테이블이 변경되었을때 얼마나 지원해줄 것인가? "

와 "얼마나 자주 변경이 일어날 것인가" 가 중요한 결정 요소라고 생각합니다.

 

비슷한 상황이 경험치 이외의 곳에서도 필요한 경우가 있습니다.

경험치와는 상황이 다르면서, 비슷한 경우는 "스킬 포인트"가 있는 경우입니다.

 

[2] 스킬 포인트

스킬 포인트는, 레벨업 과정이나 다른 방법을 통해서 획득하고,

스킬을 배우거나, 더 강하게 만들거나(레벨업이나 강화) 할때 소모한다고 가정할 경우

MAX 스킬 포인트만 저장(이것 또한 계산에 의해서 얻을 수 있는 경우도 있습니다) 하고

남아 있는 스킬 포인트를 저장할 것인가?

아니면 현재의 스킬포인트 상태를 보고 -> 사용한 스킬 포인트를 확인 ->

남은 스킬 포인트를 계산하는 형태로 할 것인가?

를 결정해야 할 수도 있습니다.

 

사실 스킬포인트의 경우 더 어려운 점이 있는데,

일부 게임에서는 이 "스킬 포인트"를 아이템 사용 형식으로 더 늘려줄수 있기 때문입니다.

이 경우는 MAX 스킬포인트를 저장 하거나

해당 아이템 사용횟수를 저장해야 하는데, 이 부분의 구현도 여러가지 선택지를 가지게 됩니다.

 

이런 선택지에 영향을 주게 되는 요소는

결국 또 스킬 개편에 의해서 어느 부분이 어떻게 얼마나 자주 변경될것인가? 에 영향을 받게 됩니다.

1) 스킬을 배우거나, 강화할때 사용하는 스킬 포인트가 변경될 경우

2) 스킬 포인트 획득 수치의 변경

3) 스킬 포인트 지급 아이템의 조정에 의한 변경

등을 고려해 볼 수 있습니다.

 

만약 스킬 포인트를 20올려주는 아이템과 50 올려주는 아이템 두 종류가 있었는데,

만약 밸런스 조정에 의해서 이걸 각각 10과 20으로 변경했을때,

기존에 이 아이템을 사용한 유저들에 대한 보정은 어떻게 해줘야할까? 등등

아주 복잡한 선택지를 가지게 됩니다.

(던X 팀에 있을때 SP책 아이템이 있을때 이런 지옥을 겪었었지요..)

 

게다가, 만약 아이템 오류나, 다른 시스템상의 오류로 이런 아이템들이 복사되어 퍼지거나,

의도치 않게 많이 획득할 수 있는 방법이 유저에게 노출되어

일부 유저가 해당 아이템을 엄청 사용했다면,

이런 부분을 쉽게 찾아서 쉽게 보정할수 있는 방법까지도 미리 고려해야 합니다.

(경험치 책은 이정도 고민은 필요 없겠지요..)

("스킬 포인트를 올려주는 아이템을 만듭시다" 라는 한 마디가 미치는 파장이 이정도로 큽니다)

 

 

이런 부분들을 보았을 때,

수치로만 저장할 것인지, 일부분은 계산된 수치값을 저장할 것인지의 결정은 상당히 어렵고,

어떤 특징을 가지는지에 대한 확실한 이해가 필요합니다.

개인적으로 가장 중요한 것은 "어떤 부분이 어떤 범위로 변경될 것인지"에 대한 범위 결정이라고 생각합니다.

 

 

posted by 지누구루
2015. 1. 27. 20:59 공부

먼저, 온라인이 아닌 게임에서의 타격 과정에 대해서 살펴 보겠습니다.

여러 액션게임중에서도 일단은 쉽게 설명하기 위해서, 1:1 대전 게임을 생각해 보겠습니다.

1:1 대전게임이면 1P의 캐릭터 A, 2P의 캐릭터 B가 있다고 가정하겠습니다.

 

일반적으로 게임은 일정 시간(대부분은 1프레임)동안 입력된 동작을 모아서

다음 프레임에 그 입력에 대한 액션을 실행하는 형태로 진행이 됩니다.

(이걸 프레임이라고 해야 하는지 정확한 용어는 모르겠지만, 내부에서는 저는 Tick이란 용어를 사용합니다. 이해를 위해서 일단 게이머들에게 익숙한 프레임이라는 표현을 사용했습니다. 게이머들에게 입력이나 화면 갱신의 최소단위가 프레임인거 같아서 입니다.)

 

온라인이 아닌 게임에서는

1) 1P가 공격 버튼을 입력 

2) 다음 프레임에 A의 공격이 나감과 동시에 타격이 발생(타격판정 발생 프레임 같은 내용은 일단 무시)

3) 같은 프레임에 데미지 계산

4) 같은 프레임에 발생한 데미지에 대한 B의 체력 게이지 화면 정보 갱신

공격의 발생부터 타격, 그리고 데미지 계산, 그리고 데미지의 표시까지 한번의 프레임에서 모두 나타낼 수 있습니다(2,3,4번 과정)

 

그렇다면 온라인 게임에서는 어떤게 달라질까요?

데미지 계산을 어떤 방식으로 하냐에 따라서 크게 달라질 수 있지만,

보통 클라이언트 해킹(변조)의 위험으로부터 보호 하기 위해 "데미지 계산"을 서버에서 하는 경우가 일반적입니다.

(충돌체크까지 서버에서 하는 경우도 있습니다)

 

그렇다면 위에서 말한 2)번 단계 이후 인 데미지 계산 -> 적용의 과정이 한 프레임에서 일어나지 않게 됩니다.

즉, 타격이 발생했으니까 처리를 해달라! 라고 서버로 요청하게 되고, 응답을 받을 때까지 시간이 흐르게 됩니다.

문제는 이게 온라인, 즉, 네트워크를 타고 움직이기 때문에, 항상 일정하지 않고, 몇 초후에 올 수도 있습니다,

이 과정에서, 온라인상에서 응답시간이 길어져서, 늦게 오게 되는 현상이 일반적으로 서버측 "렉"이라고 부르는 녀석입니다.

 

이 렉에 대한 처리를 어떤 방식으로 하느냐에 따라서, 렉이 나타나는 현상도 다르게 되는데,

제가 아는 현상중에 아래와 같은 것들이 해당됩니다.

 

1) 버튼을 눌렀는데, 공격이 한참 뒤에 나감

- 공격의 발생도 서버의 통신 후에 나가는 경우입니다. 구지 검증이나 데미지 처리가 아니더라도, 서버로 부터 응답을 받고 공격이 발생하는 경우는 이런 렉이 발생합니다.

 

2) 공격은 나갔고, 타격도 발생했는데, 데미지가 나~~중에 들어감

- 공격 발생과 타격은 서버통신을 거치지 않고 먼저 실행 하고, 데미지 계산을 서버 응답으로 처리하는 경우입니다. 보통 타격 피드백을 중요시 하는 게임에서 많이 사용하는 방법입니다.

 

3) 공격은 나갔고, 타격도 발생, 데미지도 이미 들어간 거 같은데, 나중에 갑자기 상대방의 체력이 변경됨.

(상대방의 체력을 다 깎았는데, 안죽는 경우도 이쪽 계열)

- 공격 발생과 타격, 데미지 계산까지 클라이언트에서 했지만, 서버 응답에 의해 보정을 하는 경우 입니다. 2번 경우보다 타격감이 좀 더 좋지 않나.. 하고 생각합니다.

 

 개인적인 생각으로는 약간 예외적인 케이스가 디아블로3가 아니었나 싶습니다. 특히 마법이나 화살같은 투사체의 경우인데, 공격을 쏘는 모션자체는 클라이언트에 바로 뿌려주지만, 투사체의 발생 자체가 서버렉으로 인해 늦게 나가는 경우였습니다. 정확한 방식은 발표된 바가 없어서 알 수 없지만, 투사체 발생자체는 서버검증과 계산을 거쳐서 발생하는데, 공격 모션은 이와 상관없이 재생되는 경우였다고 생각합니다.

 

 

이런 현상에서 한발짝 더 나가서 생각을 해봅시다.

사실 진짜로 하고 싶었던 이야기는 이쪽입니다 ㅎㅎ

지금까지 설명한 내용을 요약하면 'A가 B를 타격하면 서버를 거쳐서 데미지 계산이 된다'이며,

'처리방식에 따라서 서버렉이 나타나는 유형도 다르다'는 것도 간단히 설명하였습니다.

 

자 여기서 이제 1:1 게임이 아니라, 여러 캐릭터가 다수의 몬스터를 타격하는 게임에 이 방식을 적용한다고 생각해 봅시다.

특히, 범위타격의 기술을 10마리의 몬스터가 모여 있는 곳에 내려치는데, 이 공격이 초당 5회 공격을 한다고 가정해 봅시다.

단순 계산으로 아무 최적화도 거치지 않는다면. 이론상 1회의 공격으로 1초간 50개의 패킷이 발생합니다(10마리 * 5회).

10마리고 초당 5회라고 가정했을때 이정도인데,

만약에 4명의 파티원이 30마리의 몬스터가 있는 곳에 같은 기술을 동시에 사용했다고 가정하면?

4(파티원) * 30 * 5 = 600

초당 600개의 패킷이 하나의 서버에 몰리게 됩니다.

물론 이 4명만 있으면 초당 600개 패킷을 처리하는건 크게 문제가 안될 수 있지만....

동시에 플레이 하는 유저가 몇천명, 몇만명 단위가 된다면?

 

그렇기 때문에 위에 말한 내용을 최적화 하는 과정이 필요하게 됩니다.

일정 시간내 데미지 계산을 몰아서 한다던지,

스킬에 의한 총 데미지 계산은 한번에 하고 보여주는 것만 나눠서 보여준다던지 하는 방법들을 사용하게 됩니다.

하지만 이런 방법들은 실제 게임 플레이의 타격감이나, 좋은 피드백에 영향을 줄 수 있기 때문에, 실제 개발시에 어떤 방법으로 최적화 할지 결정하는 것도 중요한 요소입니다.

 

다시 한번 말하면, 서버에 의한 데미지를 계산하는 이유는, 클라이언트에서 이 계산을 하게 되면, 클라이언트 해킹으로 마음대로 데미지를 줄 수 있게 될 가능성이 열리기 때문입니다.

(이런 것 때문에 위치 검증이나, 충돌체크도 서버에서 하는 경우도 있는거라고 보시면 됩니다)

 

하지만 이런 최적화도, 결국 절대적인 타격수(패킷수) 앞에서는 장사가 없습니다.

그렇기 때문에 온라인으로 여러명의 유저가 동시에 몬스터와 싸우는 게임들은 보통

(1) 파티원의 수

(2) 동시 출현 몬스터의 수

(3) 범위 스킬인데 동시타격 개체의 수

가 제한되게 됩니다.

(또는 몬스터를 멀리 떨어뜨려 소환하고, 구역별로 처리를 따로 하는 방법이라던가)

 

이런 것을 제한하지 않고, 마음껏 할 수 있게 하고, 동시 발생 패킷수가 많아지면,

결국 서버의 초당 패킷 처리 속도를 초과하여, 서버에 처리못한 패킷이 쌓이게 되고,

결국 서버가 점점 느려져서 문제가 발생하게 됩니다.

 

물론 단순히 하나의 서버에 접속하는 접속자의 수를 제한하고,

사람이 늘어날수록 서버를 늘려서 대응하는 방법도 있습니다.

특히 요즘은 클라우드 컴퓨팅에 의해서, 서버 추가 삭제가 쉬워졌기 때문에, 이 방법도 어느정도 희망이 있지만,

반응성을 중요시하는 액션게임의 서버로 클라우드 서버를 사용하는 것은 아직은 시기 상조가 아닌가 싶습니다.

(물론 개인적인 의견입니다)

 

 

틀린 내용이 있으면 지적 감사히 받겠습니다.

 

posted by 지누구루
2015. 1. 14. 16:25 공부

정확히 어느 버전부터 가능한지는 잘 모르겠지만,

개인적으로 사용하는 디버깅 기능중에, 꽤 유용하게 사용하고 있는 기능이 있어서 정리해봅니다.

 

원래는 이런 것도 팁이냐, 당연히 다 아는거 아니냐 라고 생각했는데,

더 기본적인 것도 알려주는 글이 있길래... 요것도 한번 남겨 봅니다 ㅎㅎㅎ

 

간단한 코드부터 잠시 보겠습니다.

그냥 VS화면을 캡쳐 했습니다.

 

 

아주 간단한 코드입니다.

testArray 라는 배열을 만들고,

index가 10의 배수인 곳의 값을 10으로 세팅한 다음,

루프를 돌면서 모든 배열의 값을 출력합니다.

 

여기서 잘 보시면 두번째 for문에 마름모 모양의 브레이크 포인트를 볼수 있습니다.

그리고 그 가운데 하얗게 표시가되어 있습니다.

 

해당 위치에 F9로 브레이크 포인트를 건 뒤에

붉은색 원으로 표시되는 부분에 마우스 우클릭을 하면 몇가지 메뉴가 뜹니다.

 

그중에서 여기서 소개할 메뉴는

"적중될 때" 메뉴 입니다.

 

이 메뉴는 해당 브레이크 포인트가 걸릴때,

실행을 멈추지 않고, 해당 메뉴에서 등록된 내용을 출력창에 뿌려주는 기능입니다.

게다가 "조건" 항목과 같이 사용할 수 있기 때문에,

'특정 조건에 걸릴때, 내가 원하는 값을 출력 창에 뿌려라' 라는 명령을 할수 있습니다.

 

위의 브레이브 포인트에 설정된 값은 다음과 같습니다.

 

 

요건 조건값으로 testArray[i]에 0보다 큰 값이 들어 있는 경우에.

 

 

 

testArray Value - i : {i}, value : {testArray[i]}

를 출력해라. 라는 의미입니다.

 

창에 설명이 있지만 어떤 값을 출력하려면 {} 로 감싸서 변수를 지정하면 되고,

그 안에 들어가는 내용은 변수가 아닌 함수 호출이어도 되지만, 리턴값은 존재 해야 합니다.

느낌상으로는 {} 사이에 있는 코드를 evaluate 해서 출력하는 것 같습니다.

(evaluate를 여기서 적절히 뭐라고 한글로 적어야 할지 모르겠어서 걍 evaluate라고 씀)

 

위의 내용을 디버그 모드로 실행하면

출력창에 요렇게 찍힙니다.

 

 

조건에 맞춰서, 출력하려는 값이 출력되는걸 볼 수 있습니다.

위의 예에서는 제가 지워버렸지만,

기본적으로 제공하는 값으로, 어느 함수에서 호출되는지 같은것도 찍어줄 수 있습니다.

 

브레이크를 실제로 걸어서 멈춰버리면 멀티쓰레드 상황에서 디버깅이 너무 어려운 경우라던가,

일정 시간 이상 ping에 응답이 없으면 disconnect 처리를 해놨는데,

디버깅하다가 걸리는 일도 있었고 ...

사실 특정 조건일 때, 몇몇 상태값만 확인하면 되는데, 일일이 멈춰서 단계 따라가는게 귀찮았는데,

마침 같이 일하던 동료가 이 기능을 알려줘서 

아주 유용하게 사용하였고, 그 이후로 지금도 애용하고 있습니다.

 

별거 아닌 팁이지만, 남겨둬 봅니다 ㅎㅎㅎ

 

posted by 지누구루
2014. 12. 11. 14:51 공부

오랜만에 업무와 관련된 내용으로 글을 써봅니다.

꽤 오래전에 만든 구조이긴 한데, 현재까지 사용해본 결과 나름 괜찮은 구조라고 생각해서 글로 남겨봅니다.

 

관련된 내용은

"게임에서 아이템을 사용했을때의 효과" 를 서버측에서 필요한 기능을 구현 하는 내용입니다.

어렵지 않은 내용이고,

이미 존재하는 패턴이라서 특별히 좋은 구조다 라고 할만한 건 아닙니다.

 

일단 기본적인 생각은

1) 아이템을 사용 하고 차감하는 기능

2) 아이템을 사용함으로써 발동되는 기능

을 분리하자고 생각한 것에서 출발한 구조입니다.

 

분리하자고 느낀 이유는 가장 처음에 몸담았던 게임의 아이템 사용 구조가,

아이템에 새로운 기능을 추가할때마다, 완전히 각각 새로 구현해야 하는게 비효율적이라고 느꼈기 때문이며,

그리고 같은 기능인데, 적용하는 수치가 다르다거나 하는 정도의 변경만 가지는 새로운 아이템 추가,

또는 완전히 같은 기능인데, 아이템을 새로 만들어야 하는 경우

(같은 옵션인데 이벤트로 지급하기 위해 기간제로 만들거나 하는 경우)

그때마다, 아이템 작업을 프로그래머가 직접 해야 하는 것이 비효율적이라고 생각했기 때문입니다.

 

+ 아이템 사용쪽 코드 흐름

- 네이밍 규칙은 회사에서 사용중인 부분이 좀 있습니다. 함수 이름에 'on' 을 붙이는건 어떤 행동이 일어 났을 때 처리되어야 하는 함수를 뜻합니다. onUseItem -> 아이템이 사용되었을때 처리해야 하는 일

void useItem(playerInfo,itemInfo,parameter)
{
    // 1. 아이템 사용 조건체크 - 수량 체크나 쿨타임 체크 등등
    if( isItemUsableState(playerInfo,itemInfo) == false )
        return;

    // 2. 아이템에 연결된 기능을 실행
    if( executeItemFunction(playerInfo,itemInfo,parameter) != no_error )
        return;

    // 3. 아이템 사용 후속 조치 - 쿨타임 갱신 등등
    onUseItem(playerInfo,itemInfo);

    // 4. 사용 아이템 삭제(내용에 따라 한번에 여러개 삭제도 가능)
    deleteItem(playerInfo,itemInfo,parameter);
}

 

받는 파라메터는

playerInfo : 아이템을 사용한 플레이어 정보(캐릭터 정보가 포함될수도)

itemInfo : 사용한 아이템의 정보(현재 가지고 있는)

parameter : 아이템 사용에 필요한 추가 정보

추가 정보의 경우, 예를 들어, 장비에 사용하는 아이템이라면 해당 장비의 정보까지 포함이 됩니다. 이 내용은 아이템마다 내용이 다르므로 범용적으로 받을 수 있어야 합니다.

범용적으로 데이터를 받는 방법에는 여러가지가 있겠지만, 저는 그냥 스트링으로 넘기고 사용하는 쪽에서 파싱해서 쓰도록 했습니다.

 

"아이템에 연결된 기능이 실패하면 아이템 사용이 되지 않습니다"

 

이중에서 밑줄 그은 executeFunction() 함수의 내부는 아래와 같습니다.

error_code executeFunction(playerInfo,itemInfo,parater)
{
    functionType = itemInfo->getFunctionType();
    functionExcutor = getProperExecutor(functionType);

    return functionExecutor->execute(playerInfo,itemInfo,parameter);
}

내용을 간단히 보면.

(1) 아이템에 연결된 기능이 무엇인지 찾은 다음

(2) 해당 기능을 실행할 executor 를 찾아서

(3) 해당 executor의 기능을 실행 합니다.

 

여기서 아이템에 연결된 기능, 코드에서는 functionType만 파라메터로 사용해도 무관합니다.

사실 itemInfo는 여기까지만 존재해도 되는경우가 많습니다.

이 코드에서 itemInfo를 다 넘기는 이유는,

기능과 연결된 아이템의 정보를 다시 검증하기 위한 용도와,

기능 사용에 필요한 추가적인 데이터가 아이템 정보에 있을 경우를 위함입니다.

반드시 있어야 하는 파라메터는 아닙니다.

 

위 내용중 getProperExecutor() 함수는 함수로 빼놓기는 햇지만 큰 기능이 있는건 아니고,

type에 연결된 executor를 그냥 돌려주는 용도입니다.

저는 배열로 만들어 놓고, type에 해당하는 index에 있는 executor를 그냥 돌려주도록 했습니다.

 

이제부터는 실행되는 Function Executor에 대한 내용을 살펴볼텐데,

template method 패턴이 사용되기 때문에 미리 코드를 보고 다시 설명하겠습니다.

class CFunctionExecutor
{
private :
    playerInfo_;
    itemInfo_;

public :
    error_code execute(playerInfo,itemInfo,paramter)
    {
        itemInfo_ = itemInfo;
        playerInfo_ = playerInfo;
        if( _parseParameter(parameter) == false )
            return error_parse;

        error_code = _execute();
        _clear();

        return error_code;
    }

protected :
    virtual bool        _parseParameter(parameter){};
    virtual error_code  _execute()=0;
    virtual void        _clear() {};
};

class CEmptyExecutor : public CFunctionExecutor
{
protected :
    virtual error_code  _execute() {return no_error;}
};

 

CFunctionExecutor 가 최상위 클래스이며,

CEmptyExecutor 는 상속받아 만들어진 클래스 입니다.

(생성자,파괴자는 생략하였습니다)

 

CFunctionExecutor의 execute() 함수를 보시면

_parseParamter(), _execute(), _clear() 가 차례대로 사용되고 있습니다.

 

이중에 _execute() 함수만 순수 가상함수로, 상속 받은 클래스들은 반드시 이 함수를 구현해야 합니다.

parse와 clear의 경우는 할 필요가 없는 기능들이 많아서 빈 함수로 일단 넣어놓고,

상속받은 클래스에서 재정의 할 필요가 있으면 재정의 해서 사용하도록 하였습니다.

 

일단 아무 기능도 하지 않는 EmptyExeucotr()의 경우 parse와 clear역시 할게 없기 때문에 _execute() 함수만 재정의 하였습니다.

 

_parseParameter() 함수는 넘어온 파라메터에서 필요한 파라메터를 다시 정리하는 부분입니다.

_execute() 함수에서는 파싱된 데이터를 기반으로 기능을 실행하고

_clear() 함수에서는 기능 실행에 사용된 데이터를 초기화 하도록 합니다.

 

예를 들어, 사용할 경우 플레이어에게 버프를 주는 아이템을 만들고 싶다. 라고 하면

class CBuffExecutor : public CFunctionExecutor
{
protected :
    buff_info_;

protected :
    virtual bool        _parseParameter(parameter);
    virtual error_code  _execute();
};

bool CBuffExecutor::_parseParameter(parameter)
{
    // buff_info_ = 데이터 만들기.
}

error_code CBuffExecutor::_execute()
{
    // 버프 시스템 호출
    // CBuffSystem::buff(playerInfo_,buffinfo);
}

 

정도로 구현됩니다.

(실제 적용된 코드와는 많은 차이가 있습니다)

 

이렇게 되면 새로운 아이템 기능 추가의 경우

새로운 클래스를 CFunctionExecutor 상속받아서 생성하고

경우에 따라 _parseParameter(), _execute(), _clear() 를 구현하면 완성 됩니다.

 

이미 있는 기능을 새 아이템에 넣고 싶다면

아이템 정보에서 functionType을 같은 걸로 연결만 하면 가능합니다.

(이건 아이템 스크립트 작업자의 몫!!)

 

좀더 복잡한 기능이 필요한 경우에는,

복잡한 기능에 필요한 데이터를 parameter에 같이 넘기거나,

아이템 정보에 포함시켜 놓아서 연결해도 괜찮습니다.

 

구조 자체는 예~~~전에 보았던 Test Unit 쪽 코드와 구조가 완전히 동일합니다.....(어디서 봤더라-_-)

prepare 하고 test 하고 post 뭐시기 하고 이런 과정이었던거 같습니다.

 

글이 꽤 길어졌습니다.

이만 마무리!!!

 

 

 

posted by 지누구루
2013. 12. 3. 13:45 공부

여러 서버에서 공통으로 사용하는 스크립트 데이터를.

하나의 프로세스에서 읽어두고, 다른 프로세스들은 이미 저장된 스크립트 정보를 읽어가면 좋지 않을까 싶어서 알아 본내용(사실 개발존에서는 여러 서버를 한 머신에서 띄우기 때문에, 같은 동작을 좀 줄여줄수 있으면 개발할때 편하지 않을까 해서...)

윈도우 Named Shared Memory

 

참고한 페이지

http://msdn.microsoft.com/en-us/library/windows/desktop/aa366551(v=vs.85).aspx
http://ezbeat.tistory.com/457
http://blog.naver.com/PostView.nhn?blogId=marindie&logNo=144944471

개념.

다른 프로세스에서 공동으로 접근이 가능한 메모리 공간에 String 을 key로 하는 메모리 공간을 만들어서 값을 쓰고, 읽고, 할수 있는 Window API

 

함수.

[1] CreateFileMapping
설명 : http://msdn.microsoft.com/en-us/library/windows/desktop/aa366537(v=vs.85).aspx

공유 메모리 공간을 만드는 함수. 어떤 크기로 어떤 “String”에 연결해서 만들지를 넘겨주면, 공유 메모리를 할당하고 그 Handle을 돌려준다. Handle이 Null 일 경우 실패. 자세한 내용은 설명 페이지 참조


[2] OpenFileMapping
설명 : http://msdn.microsoft.com/en-us/library/windows/desktop/aa366791(v=vs.85).aspx

공유 메모리 공간에서 특정 “String”을 key로 저장되어 있는 메모리가 있는지 찾아서, 있으면 해당 공유 메모리의 Handle을 돌려준다. 없으면 Null. 자세한 설명은 페이지 참조


[3] MapViewOfFile
설명 : http://msdn.microsoft.com/en-us/library/windows/desktop/aa366761(v=vs.85).aspx

특정 공유 메모리 Handle에서 메모리 공간에 접근할수 있는 포인터를 돌려준다.

 

사용법.

저장하고자 하는 내용을 특정 String을 key로 CreateFileMapping 을 이용해서 공유 메모리 공간을 만들고, 만든 공간을 MapViewOfFile로 위치를 받아서 접근해서 데이터를 저장(or copy) 한다.

사용하는 쪽에서는 OpenFileMapping을 이용하여 특정 String을 Key로 공유 메모리 Handle을 얻은 다음, MapViewOfFile로 메모리 공간을 접근하여 데이터를 사용한다.

 

주의할 점.

예제를 보면 Shared Memory에 값을 올리고, getCh()로 일부러 저장하는쪽의 프로세스를 죽이지 않고 끝내고 싶을때 키 입력으로 프로세스를 내리게 되어 있다.

그래서 프로세스가 떠 있는 동안에는 동작하지만, 프로세스가 내려가면서 unmap이나 핸들을 close 하고 나면 다시 초기화 된다.

 

고민되는 점.

만약 Named Shared Memory에 데이터를 잔뜩 넣고 Handle을 close 하지 않은채 프로세스가 죽어버리면 어떻게 되나? (아마 handle 계열은 프로세스 정리할때 잘 되었던거 같기도 함). 그렇다면 만약 여러 프로세스가 접근중인 메모리를 할당했던 프로세스가 죽어버리면, 다른 프로세스는 다시 그 데이터에 접근할 수 없는가? 테스트 필요함.

=> 간단히 테스트해본 결과 역시 Handle을 Create한 프로세스가 죽으면 공유 메모리 데이터도 다 날아가네용(Handle이 해제되서 그런거 같습니당) ㅠㅠ 주의 주의

 

posted by 지누구루
2011. 12. 6. 11:36 공부

원본 :http://dev.mysql.com/tech-resources/articles/mysql-connector-cpp.html#trx

Using Transactions

A database transaction is a set of one or more statements that are executed together as a unit, so either all of the statements are executed, or none of them are executed. MySQL supports local transactions within a given client session through statements such as SET autocommit, START TRANSACTION, COMMIT, and ROLLBACK.

Disable AutoCommit Mode

By default, all the new database connections are in autocommit mode. In the autocommit mode, all the SQL statements will be executed and committed as individual transactions. The commit occurs when the statement completes or the next execute occurs, whichever comes first. In case of statements that return a ResultSet object, the statement completes when the last row of the ResultSet object has been retrieved or the ResultSet object has been closed.

One way to allow multiple statements to be grouped into a transaction is to disable autocommit mode. In other words, to use transactions, the Connection object must not be in autocommit mode. The Connection class provides the setAutoCommit method to enable or disable the autocommit. An argument of 0 to setAutoCommit() disables the autocommit, and a value of 1 enables the autocommit.

Connection *con;
..
/* disable the autocommit */
con -> setAutoCommit(0);

It is suggested to disable autocommit only while you want to be in transaction mode. This way, you avoid holding database locks for multiple statements, which increases the likelihood of conflicts with other users.

Commit or Rollback a Transaction

Once autocommit is disabled, changes to transaction-safe tables such as those for InnoDB and NDBCLUSTER are not made permanent immediately. You must explicitly call the method commit to make the changes permanent in the database or the method rollback to undo the changes. All the SQL statements executed after the previous call to commit() are included in the current transaction and committed together or rolled back as a unit.

The following code fragment, in which con is an active connection, illustrates a transaction.

Connection *con;
PreparedStatement *prep_stmt;

..
con -> setAutoCommit(0);

prep_stmt = con -> prepareStatement ("INSERT INTO City (CityName) VALUES (?)");

prep_stmt -> setString (1, "London, UK");
prep_stmt -> executeUpdate();

con -> rollback();

prep_stmt -> setString (1, "Paris, France");
prep_stmt -> executeUpdate();

con -> commit();

In this example, autocommit mode is disabled for the connection con, which means that the prepared statement prep_stmt is committed only when the method commit is called against this active connection object. In this case, an attempt has been made to insert two rows into the database using the prepared statement, but the first row with data "London, UK" was discarded by calling the rollback method while the second row with data "Paris, France" was inserted into the City table by calling the commit method.

Another example to show the alternate syntax to disable the autocommit, then to commit and/or rollback transactions explicitly.

Connection *con;
Statement *stmt;

..
stmt = con -> createStatement();

//stmt -> execute ("BEGIN;");
//stmt -> execute ("BEGIN WORK;");
stmt -> execute ("START TRANSACTION;");

stmt -> executeUpdate ("INSERT INTO City (CityName) VALUES ('London, UK')");
stmt -> execute ("ROLLBACK;");

stmt -> executeUpdate ("INSERT INTO City (CityName) VALUES ('Paris, France')");
stmt -> execute ("COMMIT;");

The START TRANSACTION or BEGIN statement starts a new transaction. COMMIT commits the current transaction to the database by making the changes permanent. ROLLBACK rolls back the current transaction by canceling the changes to the database. With START TRANSACTION, autocommit remains disabled until you end the transaction with COMMIT or ROLLBACK. The autocommit mode then reverts to its previous state.

BEGIN and BEGIN WORK are supported as aliases of START TRANSACTION for initiating a transaction. START TRANSACTION is standard SQL syntax and it is the recommended way to start an ad-hoc transaction.

Rollback to a Savepoint within a Transaction

The MySQL connector for C++ supports setting savepoints with the help of Savepoint class, which offer finer control within transactions. The Savepoint class allows you to partition a transaction into logical breakpoints, providing control over how much of the transaction gets rolled back.

As of this writing, InnoDB and Falcon storage engines support the savepoint transactions in MySQL 6.0.

To use transaction savepoints, the Connection object must not be in autocommit mode. When the autocommit is disabled, applications can set a savepoint within a transaction and then roll back all the work done after the savepoint. Note that enabling autocommit invalidates all the existing savepoints, and the Connector/C++ driver throws an InvalidArgumentException when an attempt has been made to roll back the outstanding transaction until the last savepoint.

A savepoint is either named or unnamed. You can specify a name to the savepoint by supplying a string to the Savepoint::setSavepoint method. If you do not specify a name, the savepoint is assigned an integer ID. You can retrieve the savepoint name using Savepoint::getSavepointName().

Signatures of some of the relevant methods are shown below. For the complete list of methods supported by Connection, Statement, PreparedStatement and Savepoint interfaces, check the connection.h, statement.h and prepared_statement.h headers in your Connector/C++ installation.

/* connection.h */
Savepoint* Connection::setSavepoint(const std::string& name);
void Connection::releaseSavepoint(Savepoint * savepoint);
void Connection::rollback(Savepoint * savepoint);

The following code fragment inserts a row into the table City, creates a savepoint SAVEPT1, then inserts a second row. When the transaction is later rolled back to SAVEPT1, the second insertion is undone, but the first insertion remains intact. In other words, when the transaction is committed, only the row containing "London, UK" will be added to the table City.

Connection *con;
PreparedStatement *prep_stmt;
Savepoint *savept;

..
prep_stmt = con -> prepareStatement ("INSERT INTO City (CityName) VALUES (?)");

prep_stmt -> setString (1, "London, UK");
prep_stmt -> executeUpdate();

savept = con -> setSavepoint ("SAVEPT1");

prep_stmt -> setString (1, "Paris, France");
prep_stmt -> executeUpdate();

con -> rollback (savept);
con -> releaseSavepoint (savept);

con -> commit();

The method Connection::releaseSavepoint takes a Savepoint object as a parameter and removes it from the current transaction. Any savepoints that have been created in a transaction are automatically released and become invalid when the transaction is committed, or when the entire transaction is rolled back. Rolling back a transaction to a savepoint automatically releases and invalids any other savepoints that were created after the savepoint in question. Once a savepoint has been released, any attempt to reference it in a rollback operation causes the SQLException to be thrown.

posted by 지누구루
2011. 2. 14. 17:24 공부


http://crowmaniac.net/crowmania/?p=54

부스트의 바인드 설명글.


boost 라이브러리에 쓸만한 게 굉장히 많은데.
전혀 몰랐던 내용이 많이 있다.


읽어들 두시면 많은 도움이 될 듯.


boost::asio 예제코드만 보고. 헤깔려 하다가.
위 페이지의 설명이 매우 도움 되었음.

posted by 지누구루
2010. 11. 1. 00:46 공부

이 책을 산게 언제였더라.
8월초였던거 같은데... 7월말이었나?-_-+

무려 3개월이나 지난 오늘 다 읽었다.


박일님 번역이라서 믿고 샀던 것도 있었는데.
역시나 번역의 품질이 매우. 아주 심하게 좋습니다.

적절하게 예시를 주석으로 달아주시거나.
잘 모르는 단어에 대해 설명을 추가해 놓은 점도 아주 마음에 듭니다.


내용에 대해서 말하자면.
아마도 회사에서 몇개월만이라도 일해본 프로그래머라면 모두 심히 공감할만한 내용이며.
특정 내용을 읽을때면 자신의 경험과 오버랩되면서
아... 하는 회상에 잠기게도 만들어 주는. 공감 100% 책이었습니다.


그리고 집고 넘어갈 것이.
실제로 자신이 버그에 대처하는 자세에 대해 많은 생각을 가지게 하고.
지금까지 잘못 생각했던 것들에 대한 경각심을 일깨워 줍니다.

책의 거의 끝 부분인 11장에. 흑마술. 이란 제목의 글에 이런 이야기가 나옵니다.

'맞아요. 무슨 이유인지 모르겠지만 그 서버에서 빌드한 바이너리에서는 항상 그 버그가 생기더라구요. 하여간 꼭 다른 서버에서 빌드하세요'

'아하, 그 에러가 생겼나 보네요. 시작할 때 순서를 잘 맞춰줘야 해요. 원래는 순서가 다르다고 해서 결과가 달라지면 안되는데, 이상하게 에러가 생기더군요'

'뭐, 처음에는 항상 실패하긴 하는데, 그 다음에는 무조건 성공하니까 너무 신경쓰지 말아요'

이런 부분을 꼼꼼하게. 잘 챙기고 끝까지 버그를 추적해야 한다.는 해결책을 제시하고 있는데.
실제로 저런 경우가 굉장히 많았다.

"무슨 이유에선지 모르겠는데 이코드만 추가하면 서버가 죽어요"    라던가.

"무슨 이유에선지 모르겠는데 실섭에서만 패킷이 지연되요" 같은.

그리고 어느샌가 아무것도 고치지 않았는데 잘 동작한다며. 그냥 지나친 것들.


분명히 그건 반성해야 할 것들인것 같다.


이책은
주기적으로 읽어주면.
지속적으로 자신의 버그에 대한 자세를 다시 잡을수 있는.
그런 책이 되어 줄것 같다.

posted by 지누구루
prev 1 2 next