디시인사이드 갤러리

갤러리 이슈박스, 최근방문 갤러리

갤러리 본문 영역

적분시리즈: 리듬게임을 만들수 있을까? 10. 노트 내려오게 하기

∫ 2t dt=t²+c갤로그로 이동합니다. 2009.07.23 00:23:10
조회 256 추천 0 댓글 12




이제 리듬게임에 핵심이라고 할 수 있는 노트에 대한 처리를 해보자.
먼저 노트에 대한 데이터를 담을 수 있는 구조체를 선언하자.


struct NoteData
{
    int time; //시간
    BYTE type; //종류
    BYTE state; //상태
    WORD data; //데이터
    GObject obj; //그리기객체
};
enum{
    ND_Note1=0,
    ND_Note2,
    ND_Note3,
    ND_Note4,
    ND_Note5,
    ND_Button1Sound,
    ND_Button2Sound,
    ND_Button3Sound,
    ND_Button4Sound,
    ND_Button5Sound,
    ND_LongNote1S,
    ND_LongNote2S,
    ND_LongNote3S,
    ND_LongNote4S,
    ND_LongNote5S,
    ND_LongNote1E,
    ND_LongNote2E,
    ND_LongNote3E,
    ND_LongNote4E,
    ND_LongNote5E,
    ND_Bar,
    ND_Vel,
};

간단하게 설명하자면
노트가 내려올 시간을 담는 time,
노트의 종류를 담는 type,
노트의 처리상태(제때에 눌러졌는가? 아니면 놓쳤는가?)를 담는 state
기타 데이터를 담는 data,
노트의 그리기객체인 obj가 구조체를 구성한다.

type에 대한 열거형값이 아래에 정의되어 있다.
ND_Note1은 제일 왼쪽에서 내려오는 노트,
ND_Note2는 두 번째에서 내려오는 노트,
...
ND_Note5는 제일 오른쪽에서 내려오는 노트이고,
ND_Button1~5Sound는 노트 정보는 아니지만 버튼의 소리를 설정한다.
ND_LongNote1~5S는 기다란 노트의 시작점,
ND_LongNote1~5E는 기다란 노트의 끝점이다.
ND_Bar는 마디구분선,
ND_Vel은 내려오는 속도를 설정한다.

이제 매 프레임 루프를 돌면서 시간이 된 데이터를 처리하고 화면에 보여주면 된다.


class MPlayGame : public MState
{
protected:
    struct{
        POINT frame;
        POINT key[5];
        POINT notelt;
        POINT noterb;
    }m_ui;
    vector<BYTE> m_codedata;
    GObject m_objframe;
    GObject m_button[5];
    vector<NoteData> m_notes;
    size_t m_notebegin;
    size_t m_noteend;
    WORD m_buttonsound[5];
    int m_time;
    float m_vspeed;
    BYTE m_buttonkeycode[5];
    enum{
        Event_Frame=0,
        Event_NoteA,
        Event_NoteB,
        Event_NoteC,
        Event_NoteAEnd,
        Event_NoteBEnd,
        Event_NoteCEnd,
        Event_Button1Down,
        Event_Button5Down=Event_Button1Down+4,
        Event_Button1Up,
        Event_Button5Up=Event_Button1Up+4,
    };
    inline int GetButton(int KeyCode);

    inline void BeginNote(NoteData* nd);
    inline void EndNote(NoteData* nd);
    inline POINT PosNote(NoteData* nd);
    inline void PassNote(NoteData* nd);
    inline void BeginBar(NoteData* nd);
    inline void EndBar(NoteData* nd);
    inline POINT PosBar(NoteData* nd);
    inline void PassButtonSound(NoteData* nd);
    inline void PassVel(NoteData* nd);
    inline void ButtonDown(int Button);
    inline void ButtonUp(int Button);
public:
    MPlayGame(GGame* pGame);   
    ~MPlayGame();
    void OnProc();
    void OnDraw();
    void OnPostProc();
    void OnLost();
    void ();
    void (int KeyCode);
    void (int KeyCode);
    void OnDrawSkip();
    GRET LoadOC(MemFile* file);

};

MPlayGame클래스에 이것저것이 많이 추가되었다.

m_button[5]은 5개의 버튼이 눌렸을때 나타나는 그래픽의 그리기객체이고,
m_notes는 노트데이터를 담는 변수이다.

m_notebegin과 m_noteend는 노트를 검색할 범위를 나타낸다.
노트데이터가 많으면 매 프레임마다 전체를 검색하는데 시간이 꽤 걸린다. 쓸데없는데에 CPU를 낭비하지 말자.
노트데이터를 시간순으로 정렬해 놓는다면, 시간에 따라서 지나간 부분, 아직 오지 않은 부분은 버리고 검색할 수 있으므로 좀 더효율적이다. 여기서 검색을 시작하는 위치가 m_notebegin이고 검색을 끝내는 위치가 m_noteend이다.

m_buttonsound[5]는 5개의 버튼이 눌렸을때 나는 효과음의 인덱스를 저장한다. 물론 아직 사운드부분이 완성되지 않아서 실질적으로는 쓸모가 없다.

m_time은 현재 진행되고 있는 음악의 시간이고,
m_vspeed는 노트가 떨어지는 속도이다.
m_buttonkeycode[5]는 5개의 버튼이 대응되는 키의 키코드이다.

그리고 그 아래에는 Event_어쩌구 해서 그리기코드에 정의되어있는 이벤트들이 열거되어있다.

그 아래 inline함수로 내부 구현 함수들이 정의되어있다.

GetButton함수는 키코드를 통해 이 키가 1~5 버튼 중 어느 버튼에 해당하는지 리턴한다.

Begin/EndNote는 노트가 보이기 시작할때, 사라졌을때 호출되는 함수이다.
Begin/EndBar는 마디구분줄이 보이기 시작할때, 사라졌을때 호출되는 함수,
PosNote/Bar는 노트와 마디구분줄을 그릴때 위치를 얻기 위해 호출되는 함수이다.
PassNote/ButtonSound/Vel은 노트/버튼소리변경/속도변경 이벤트가 지나갈때 호출되는 함수,
ButtonDown/Up은 버튼이 눌려졌을때, 떼어졌을때 호출되는 함수이다.


int MPlayGame::GetButton(int KeyCode)
{
    for(int i=0;i<5;i++)
    {
        if(m_buttonkeycode[i]==KeyCode)return i;
    }
    return -1;
}

GetButton함수 구현을 보자. m_buttonkeycode에서 검색을 해서 해당하는 KeyCode가 있으면 그 버튼의 번호를 리턴하고, 없으면 -1을 리턴한다.


void MPlayGame::OnProc()
{
    int vfstart=m_ui.notelt.y-m_ui.key[0].y-50;
    int vfend=m_ui.noterb.y-m_ui.key[0].y+50;
    while(m_noteend<m_notes.size() &&
        m_vspeed*(m_time-m_notes[m_noteend].time)>vfstart)
    {
        switch(m_notes[m_noteend].type)
        {
        case ND_Note1:
        case ND_Note2:
        case ND_Note3:
        case ND_Note4:
        case ND_Note5:
            BeginNote(&m_notes[m_noteend]);
            break;
        case ND_Bar:
            BeginBar(&m_notes[m_noteend]);
            break;
        }
        m_noteend++;
    }
    for(size_t i=m_notebegin;i<m_noteend;i++)
    {
        if(m_vspeed*(m_time-m_notes[i].time)>vfend)
        {
            switch(m_notes[i].type)
            {
            case ND_Note1:
            case ND_Note2:
            case ND_Note3:
            case ND_Note4:
            case ND_Note5:
                EndNote(&m_notes[i]);
                break;
            case ND_Bar:
                EndBar(&m_notes[i]);
                break;
            }
            m_notebegin++;
        }
        if(m_time>m_notes[i].time && m_notes[i].state==0)
        {
            switch(m_notes[i].type)
            {
            case ND_Note1:
            case ND_Note2:
            case ND_Note3:
            case ND_Note4:
            case ND_Note5:
                PassNote(&m_notes[i]);
                break;
            case ND_Button1Sound:
            case ND_Button2Sound:
            case ND_Button3Sound:
            case ND_Button4Sound:
            case ND_Button5Sound:
                PassButtonSound(&m_notes[i]);
                break;
            case ND_Vel:
                PassVel(&m_notes[i]);
                break;
            }
            m_notes[i].state=1;
        }
    }
}

먼저 m_noteend부분부터 검색을 시작하여 보여져야하는 노트/들은 BeginNote/Bar함수를 호출해서 드러낸다.
그 뒤 m_notebegin부분에서 검색하여 지나간 노트/들은 EndNote/Bar함수를 호출해서 숨긴다.
그리고 처리시간이 된 노트/들은 PassNote/ButtonSound/Vel함수를 호출해서 처리할 이벤트를 처리한다.


void MPlayGame::(int KeyCode)
{
    int ibut=GetButton(KeyCode);
    if(ibut>=0)ButtonDown(ibut);
}

void MPlayGame::(int KeyCode)
{
    int ibut=GetButton(KeyCode);
    if(ibut>=0)ButtonUp(ibut);
}

키이벤트처리는 간단하게 GetButton함수로 해당하는 버튼이 있는지 확인하여 ButtonDown/Up함수를 호출해준다.


void MPlayGame::OnDraw()
{
    m_objframe.Proc(m_pGame, m_ui.frame, m_ui.key);
    for(size_t i=m_notebegin;i<m_noteend;i++)
    {
        POINT t={0, (int)(m_vspeed*(m_time-m_notes[i].time))};
        switch(m_notes[i].type)
        {
        case ND_Note1:
        case ND_Note2:
        case ND_Note3:
        case ND_Note4:
        case ND_Note5:
            m_notes[i].obj.Proc(m_pGame, m_ui.frame+PosNote(&m_notes[i])+t, NULL);
            break;
        case ND_Bar:
            m_notes[i].obj.Proc(m_pGame, m_ui.frame+PosBar(&m_notes[i])+t, NULL);
            break;
        }
    }
    for(int i=0;i<5;i++)
    {
        m_button[i].Proc(m_pGame, m_ui.frame+m_ui.key[i], NULL);
    }
}

그리는 부분에서는 노트데이터의 그리기객체와 버튼의 그리기객체를 처리한다.


void MPlayGame::OnPostProc()
{
    m_time++;
}

후처리부분에서는 m_time을 1증가시킨다.


void MPlayGame::BeginNote(NoteData *nd)
{
    nd->obj.Init(&m_codedata[0]);
    switch(nd->type)
    {
    case ND_Note1:
    case ND_Note5:
        nd->obj.Jump(Event_NoteA*3);
        break;
    case ND_Note2:
    case ND_Note4:
        nd->obj.Jump(Event_NoteB*3);
        break;
    case ND_Note3:
        nd->obj.Jump(Event_NoteC*3);
        break;
    }
}

void MPlayGame::EndNote(NoteData *nd)
{
    nd->obj.End();
}

노트가 화면에 보일때는 그리기객체를 초기화하고 적당한 이벤트로 점프하여 그리기를 준비하고,
노트가 사라질때는 그리기객체를 소멸시킨다.


POINT MPlayGame::PosNote(NoteData *nd)
{
    switch(nd->type)
    {
    case ND_Note1:
        return m_ui.key[0];
    case ND_Note2:
        return m_ui.key[1];
    case ND_Note3:
        return m_ui.key[2];
    case ND_Note4:
        return m_ui.key[3];
    case ND_Note5:
        return m_ui.key[4];
    }
    return POINT();
}

노트의 위치를 정하는 함수에서는 노트의 상대적 위치를 리턴해준다.


void MPlayGame::ButtonDown(int Button)
{
    m_button[Button].Init(&m_codedata[0]);
    m_button[Button].Jump((Event_Button1Down+Button)*3);
    for(size_t i=m_notebegin;i<m_noteend;i++)
    {
        if(m_notes[i].type==ND_Note1+Button && (m_time-m_notes[i].time)>-15 && (m_time-m_notes[i].time)<0)
        {
            m_notes[i].state=1;
            switch(m_notes[i].type)
            {
            case ND_Note1:
            case ND_Note5:
                m_notes[i].obj.Jump(Event_NoteAEnd*3);
                break;
            case ND_Note2:
            case ND_Note4:
                m_notes[i].obj.Jump(Event_NoteBEnd*3);
                break;
            case ND_Note3:
                m_notes[i].obj.Jump(Event_NoteCEnd*3);
                break;
            }
            break;
        }
    }
}

void MPlayGame::ButtonUp(int Button)
{
    m_button[Button].Jump((Event_Button1Up+Button)*3);
}

버튼이 눌러졌을때는 버튼 이펙트의 그리기 객체를 초기화하고,
15프레임이내의 시간에 노트가 있으면 노트가 눌러졌음을 처리한다.

이제 그리기코드를 프로그래밍해보자.

goto FrameInit
goto NoteA
goto NoteB
goto NoteC
goto NoteAEnd
goto NoteBEnd
goto NoteCEnd
goto Key1Down
goto Key2Down
goto Key3Down
goto Key2Down
goto Key1Down
goto Key1Up
goto Key2Up
goto Key3Up
goto Key2Up
goto Key1Up

FrameInit:
setdata 0 9 469
setdata 1 59 469
setdata 2 110 460
setdata 3 194 469
setdata 4 245 469
setdata 5 0 0
setdata 6 0 600

Frame:
setdrawmode normal RGBA
draw 0 0 0 0xffffffff
frameend
goto Frame

NoteA:
setdrawmode normal RGBA
draw 1 0 0 0xffffffff
setdrawmode add RGBA
draw 1 0 0 0xffffffff
frameend
setdrawmode normal RGBA
draw 1 0 0 0xffffffff
setdrawmode add RGBA
draw 1 0 0 0xe0ffffff
frameend
......
goto NoteA

NoteAEnd:
setdrawmode normal RGBA
draw 1 0 0 0xff707070
frameend
goto NoteAEnd

NoteB:
setdrawmode normal RGBA
draw 2 0 0 0xffffffff
setdrawmode add RGBA
draw 2 0 0 0xffffffff
frameend
setdrawmode normal RGBA
draw 2 0 0 0xffffffff
setdrawmode add RGBA
draw 2 0 0 0xe0ffffff
frameend
......
goto NoteB

NoteBEnd:
setdrawmode normal RGBA
draw 2 0 0 0xff707070
frameend
goto NoteBEnd

NoteC:
setdrawmode normal RGBA
draw 3 0 0 0xffffffff
setdrawmode add RGBA
draw 3 0 0 0xffffffff
frameend
setdrawmode normal RGBA
draw 3 0 0 0xffffffff
setdrawmode add RGBA
draw 3 0 0 0xe0ffffff
frameend
......
goto NoteC

NoteCEnd:
setdrawmode normal RGBA
draw 3 0 0 0xff707070
frameend
goto NoteCEnd

Key1Down:
setdrawmode add RGBA
draw 4 -13 -10 0x20ff0000
frameend
setdrawmode add RGBA
draw 4 -13 -10 0x60ff0000
frameend
......
Key1DownLoop:
setdrawmode add RGBA
draw 4 -13 -10 0xffff0000
frameend
setdrawmode add RGBA
draw 4 -13 -10 0xf0ff0000
frameend
setdrawmode add RGBA
draw 4 -13 -10 0xe0ff0000
frameend
goto Key1DownLoop

Key1Up:
setdrawmode add RGBA
draw 4 -13 -10 0xffff0000
frameend
setdrawmode add RGBA
draw 4 -13 -10 0xf0ff0000
frameend
......
setdrawmode add RGBA
draw 4 -13 -10 0x10ff0000
frameend
end

Key2Down:
setdrawmode add RGBA
draw 4 -13 -10 0x200000ff
frameend
setdrawmode add RGBA
draw 4 -13 -10 0x600000ff
frameend
......
Key2DownLoop:
setdrawmode add RGBA
draw 4 -13 -10 0xff0000ff
frameend
setdrawmode add RGBA
draw 4 -13 -10 0xf00000ff
frameend
setdrawmode add RGBA
draw 4 -13 -10 0xe00000ff
frameend
goto Key2DownLoop

Key2Up:
setdrawmode add RGBA
draw 4 -13 -10 0xff0000ff
frameend
setdrawmode add RGBA
draw 4 -13 -10 0xf00000ff
frameend
......
setdrawmode add RGBA
draw 4 -13 -10 0x100000ff
frameend
end

Key3Down:
setdrawmode add RGBA
draws 4 -13 -10 1.6 1.6 0x2000ff00
frameend
setdrawmode add RGBA
draws 4 -13 -10 1.6 1.6 0x6000ff00
frameend
......
Key3DownLoop:
setdrawmode add RGBA
draws 4 -13 -10 1.6 1.6 0xff00ff00
frameend
setdrawmode add RGBA
draws 4 -13 -10 1.6 1.6 0xf000ff00
frameend
setdrawmode add RGBA
draws 4 -13 -10 1.6 1.6 0xe000ff00
frameend
goto Key3DownLoop

Key3Up:
setdrawmode add RGBA
draws 4 -13 -10 1.6 1.6 0xff00ff00
frameend
setdrawmode add RGBA
draws 4 -13 -10 1.6 1.6 0xf000ff00
frameend
......
setdrawmode add RGBA
draws 4 -13 -10 1.6 1.6 0x1000ff00
frameend
end

굉장히 길다. 하지만 별뜻은 없고 언제 어떤 그림을 그려야 하는지 지정한 것이다. 윗부분의 goto 리스트들이 열거형으로선언했던 Event_Frame, Event_NoteA... 들에 대응하는 부분이다. 즉 그리기객체에서 Jump(Event_Frame*3 ) 하면 그리기코드의 FrameInit:으로 가게되는 것이다.

자세한 것은 첨부파일에 있는 acc 폴더안에 들어있다.
acc안에 있는 프로그램들로 sprites.txt, textures.txt, obj.txt를 변환하여 실행파일과 같은 폴더에 넣고 실행해보자. (변환하는 방법은 8강에서 설명했다.)

14119F104A672DCC07AC29
우왕! 드디어 노트가 내려온다.

추천 비추천

0

고정닉 0

0

댓글 영역

전체 댓글 0
등록순정렬 기준선택
본문 보기

하단 갤러리 리스트 영역

왼쪽 컨텐츠 영역

갤러리 리스트 영역

갤러리 리스트
번호 제목 글쓴이 작성일 조회 추천
설문 어떤 상황이 닥쳐도 지갑 절대 안 열 것 같은 스타는? 운영자 24/05/20 - -
146844 이쯤에서 적절한 변비 이야기 [3] 아주아슬갤로그로 이동합니다. 09.09.08 62 0
146843 [진지] 횽들 목표를 뭘로 정해야 할까? [4] prismatic갤로그로 이동합니다. 09.09.08 71 0
146842 폭풍설사 핵설사? 까지 말라고 그래 [7] 심심이(203.248) 09.09.08 89 0
146841 너님들도 이럴때 쥬넨 짜증날껄?? [1] 개쉛기갤로그로 이동합니다. 09.09.08 72 0
146839 면접갔다왔는데;;;; [1] ㅇㅇ(211.243) 09.09.08 101 0
146838 정보처리산업기사 실기 난이도 어때? ㅇㄷㅇㅇ(116.127) 09.09.08 280 0
146836 유리한횽은 봅니다 [8] Vita500갤로그로 이동합니다. 09.09.08 79 0
146835 폭풍설사의 미학 [6] 심심이(203.248) 09.09.08 104 0
146834 폭풍설사 안해봤으면 말을하지 마세요 [5] 숙신갤로그로 이동합니다. 09.09.08 101 0
146833 동안과 노안 [8] 관심시전갤로그로 이동합니다. 09.09.08 115 0
146832 API 블럭깨기에서 블럭 충돌체크 뭘로해야되 [2] 홍콩진호갤로그로 이동합니다. 09.09.08 104 0
146831 학교 다니는 재미. [4] 혼아갤로그로 이동합니다. 09.09.08 91 0
146830 트위터ㅋㅋㅋㅋㅋㅋㅋㅋ [7] ㅇㅇㅃ갤로그로 이동합니다. 09.09.08 97 0
146828 어제 이력서 작성했는데 [4] Q Lazzarus갤로그로 이동합니다. 09.09.08 103 0
146826 c언어 처음부터 하려니 머리 아풔 [4] (219.240) 09.09.08 78 0
146825 프갤문학상 - 도느반. [10] 피로토스갤로그로 이동합니다. 09.09.08 310 0
146824 때는 2010년 X-men들이 한국을 상륙한다. [1] 씬입사원갤로그로 이동합니다. 09.09.08 55 0
146823 도느반이 소설가라는게 true 임?? [3] 개쉛기갤로그로 이동합니다. 09.09.08 100 0
146819 요즘 플렉스가 대세야 [4] 도느반갤로그로 이동합니다. 09.09.08 138 0
146818 재범이에 관해서 [8] Vita500갤로그로 이동합니다. 09.09.08 100 0
146816 맑탉앉앍꿇헒횽은 봅니다 [1] Vita500갤로그로 이동합니다. 09.09.08 62 0
146815 매장 직원 교육용이래 [4] 분당살람갤로그로 이동합니다. 09.09.08 88 0
146814 열심히 하려는 후임을 내손으로 짤라야만 하는 마음 [10] 도느반갤로그로 이동합니다. 09.09.08 257 0
146813 암호화 할때 키는 어디에 저장하나요? [6] ㅇㄹ(218.53) 09.09.08 99 0
146811 쿼리문제 답이다. [1] 피로토스갤로그로 이동합니다. 09.09.08 68 0
146810 뇌자알 소환 캐스팅 완료 [9] ㅇㅇㅃ갤로그로 이동합니다. 09.09.08 142 0
146808 난 첫회사에서 짤릴때. [3] rntjr갤로그로 이동합니다. 09.09.08 189 0
146807 쿼리문제인데 답변 좀.. [3] ㅈㅈ(210.94) 09.09.08 59 0
146806 어제 여자 후배가 [4] 유리한갤로그로 이동합니다. 09.09.08 169 0
146805 난 첫회사에서 사직할때 [3] 하이애나갤로그로 이동합니다. 09.09.08 103 0
146803 횽들 질문점... [6] 신발라마갤로그로 이동합니다. 09.09.08 57 0
146800 회사에 일본 여자 사귀고 있는 횽아가 하나 있는데... [5] 물속의다이아갤로그로 이동합니다. 09.09.08 188 0
146797 회사를 그만 두려고 심각하게 고민중입니다. [7] fguy(211.192) 09.09.08 174 0
146795 오예씨발 하느님 감사합니다! [4] 맑탉앉앍꿇헒갤로그로 이동합니다. 09.09.08 134 0
146793 어제 여자 후배가 [23] 숙신갤로그로 이동합니다. 09.09.08 369 0
146792 재범이 얘 결국 탈퇴하네 [9] ㅇㅇㅃ갤로그로 이동합니다. 09.09.08 125 0
146791 내가 그 개새끼다 [2] LightEach갤로그로 이동합니다. 09.09.08 116 0
146790 요즘 날씨 봐서는 [3] 숙신갤로그로 이동합니다. 09.09.08 55 0
146789 나도 예전에 상무님한테 그만두겠다고 했었어. [3] 심심이(203.248) 09.09.08 113 0
146785 어젠 겉저리를 담가 먹었다능 [2] Tathagata갤로그로 이동합니다. 09.09.08 55 0
146784 자신의 닉네임에 대해 좀 더 신중할 필요성이있다. [14] 개쉛기갤로그로 이동합니다. 09.09.08 182 0
146781 일본의 전자회사들은 분연히 떨쳐일어나 새 CPU 를 만들기로 결의하였다. [2] 때릴꺼야?(116.40) 09.09.08 158 0
146780 정보처리기사 실기 옛날처럼 프로그래밍 도입했으면.. [6] 컹곰(124.80) 09.09.08 204 0
146779 ㅇㅇㅃ횽은 봅니다 [2] Vita500갤로그로 이동합니다. 09.09.08 45 0
146778 잠깐.. 프갤고정 sh횽과 .sh 는 다른 인물인듯? [16] 유리한갤로그로 이동합니다. 09.09.08 175 0
146776 점심 먹으러 가기 전에.. 뻘글 2nd [7] .sh(122.36) 09.09.08 96 0
146775 Vita횽아에게 질문... [2] 물속의다이아갤로그로 이동합니다. 09.09.08 70 0
146774 홈페이지 다 만들었다. [2] Vita500갤로그로 이동합니다. 09.09.08 99 0
146772 세상이 점점 바람지케지고 있다 [5] 분당살람갤로그로 이동합니다. 09.09.08 104 0
146771 우리 회사에 OCP 자격증을 가진 처자가 하나 있는데... [6] 물속의다이아갤로그로 이동합니다. 09.09.08 310 0
갤러리 내부 검색
제목+내용게시물 정렬 옵션

오른쪽 컨텐츠 영역

실시간 베스트

1/8

뉴스

디시미디어

디시이슈

1/2