Click here to Skip to main content
15,881,803 members
Articles / Multimedia / GDI

A Bicho Hunting Multiplayer Game

Rate me:
Please Sign up or sign in to vote.
4.09/5 (6 votes)
18 Feb 2010CPOL7 min read 20.6K   972   13   3
A practice of using Windows GDI and Winsock.
screen shot screen shot

Download executable.zip - 1.35 MB

Download src.zip - 1.36 MB

Download src_en_string.zip - 17.59 KB
This zip file contain 4 files:
Dialogs.cpp
GameEntry.cpp
Hud.cpp
Resource.rc
These files print English texts instead of Chinese texts, you can use them to replace the files in original zip(src.zip).


Introduction

A practice of using Windows GDI and Winsock.

Background

Months ago I decide to write a program with network capability. I then visited Code Project dot com and found a interesting game called Bicho Hunting:
bichohunting.aspx

I used two materials from Dr. Emmett Brown's Bicho Hunting:
1. src\bmp\char.bmp
2. src\bmp\title.bmp

I chose it and improved it, now it can play among players.

Thanks Dr. Emmett Brown for his excellent work.

This game is different from his.

Points of Interest

I didn't define any class at all. If std::vector was not used, this should be a C program. Perhaps I did used some C++ features since this is C++ code, I believe the use of C++ features is not much (if any).

In this game you use arrow keys to move your icon in screen, spacebar to fire. Mouse buttons is used to switch between fullscreen and maximize.

About This Game

The main idea of this game is to produce two perpendicular beam lines and let these lines hit something (sometimes avoid hit something, eg. you should always avoid hit skulls).

To increase the difficulty, your target is designed as moving object, but their motion trace was not randomized, they are just uniform linear motions with bounce. There has another type of moving object, the player. As your opponents, their motion trace could considered randomized, hit them and avoid be hit by them is the thing you should do.

Game Loop

This game is message-driven. The engine of this game is a timer that periodically posts GM_UPDATE message to main thread's message queue. I used timeBeginPeriod() to setup the timer, it's in another thread. The only task of this thread is to post GM_UPDATE message, so there's no need to worry about thread synchronization.

Let's take a look at an actual game loop.

When the game first run, it wants to get some information from you, eg. host or join.

DShowMainMenu((LPARAM)&g_gci);

We choose to create a game, other settings use default. Then the main window was created. In WM_CREATE we call the function MRunMap(). Now, map level and map step is zero, so we reached here:

VOID MRunMap()
{
    switch (LOWORD(g_gci.dwGameMode))
    {
    case GMODE_COOP:
        // ...
    case GMODE_FAIR:
        // ...
    case GMODE_TEAM:
        // ...
    case GMODE_DUEL:
        // ...
    }
}

Game mode is default to FAIR, game level is zero, we reached here:

case GMODE_FAIR:
    switch (g_map.level)
    {
    case 1: MFAIRStage1(); break;
    case 2: MFAIRStage2(); break;
    case 3: MFAIRStage3(); break;
    case 4: MFAIRStage4(); break;
    case 5: MFAIRStage5(); break;

    default:
        g_map.level = rand() % 5 + 1;
        timeKillEvent(g_uTimerID);
        g_uTimerID = timeSetEvent(g_uDelay, TTL_UPERIOD,
            SFinalDispatcher, NULL, TIME_PERIODIC | TIME_CALLBACK_FUNCTION);
        break;
    }

level is zero so matches the "default" branch, assume level is set to 1.

In MFAIRStage1(), we first stop the timer (if there exists a running one). Because the step is zero, we know it's time to setup the map, eg. set background graphic, generate balls (Bichos), place players, synchronize these informations to all players (clients). Finally, we set step to 1, and start the timer. 40 milliseconds later, we will receive a GM_UPDATE message and MFAIRStage1() shall be called, this time the step is 1, we are running main game logic.

When mission accomplished, MFAIRStage1() sets level and step to zero, after, similar process will be repeated.

In a COOP game, when mission accomplished, say, MCOOPStage1(), increases level by one and sets step to zero, in next time tick, MCOOPStage2() will be called, step 0 will be executed, similar to MCOOPStage1(). If mission failed, sets step to zero but keep the level, next time tick will restart the map.

Drawing On A Static Control

To archive this goal you must let the static control itself update its client area first. I learnt this technology from Charles Petzold's "Programming Windows", 5ed. Figure 11-3. The ABOUT2 program. In this game it's implemented like this:

// case WM_PAINT:

InvalidateRect(hwndLogo, NULL, TRUE);
UpdateWindow(hwndLogo);

hDC = GetDC(hwndLogo);
TransparentBlt(hDC, 0, 0, 430, 160, hdcLogo, 0, 0, 430, 160, RGB(255,0,255));
ReleaseDC(hwndLogo, hDC);

// break;

Position Synchronization

Since this is a game with network capability, we must ensure every moving object has the same relative position to all players that may have different screen resolutions. The solution is run game in a logic rectangle that has a width of 1024 pixels and a height of 768 pixels. Then use StretchBlt() to paste it onto player's screen.

Background graphic

Mostly, I use a function named MSimpleFade8x768 to generate a background graphic. This function first generate a 8x768 strip then StretchBlt it to logic rectagle, code like this:
HBITMAP hbmSrc = CreateCompatibleBitmap(hDC, 8, SIZE_LOGICY);
SelectObject(hdcSrc, hbmSrc);
SelectObject(hdcSrc, GetStockObject(DC_BRUSH));

for (i = 0; i < 768; i += 8)
{
    SetDCBrushColor(hdcSrc, RGB(r, g, b));
    PatBlt(hdcSrc, 0, i, 8, 8, PATCOPY);

    // decreasement R value or G value or B value or two of them or all
    // till they equal to zero
}

StretchBlt(hDC, 0, 0, SIZE_LOGICX, SIZE_LOGICY, hdcSrc, 0, 0, 8, SIZE_LOGICY, SRCCOPY);
I learnt this technology from Charles Petzold's "Programming Windows", 5ed. Figure 16-7. The SYSPAL3 program.

Rectangle

Rectangle is a part of Windows GDI.

>>> Objective:
To place a smaller rectangle into a bigger rectangle.

>>> Solution:
There is not too much to say.

VOID MPlacePlayer(PRECT prc, CONST PRECT prcBig)
{
    int x, y, w, h, wBig, hBig;

    w = prc->right - prc->left;
    h = prc->bottom - prc->top;

    wBig = prcBig->right - prcBig->left;
    hBig = prcBig->bottom - prcBig->top;

    x = rand() % (wBig - w);
    y = rand() % (hBig - h);

    SetRect(prc, x, y, x + w, y + h);
    OffsetRect(prc, prcBig->left, prcBig->top);
}

>>> Objective:
To draw a block of text.

>>> Solution:
First we need to know the area of the rectangle, this is accomplished by DrawText with its uFormat parameter combined with a DT_CALCRECT flag. Assume we already know the width of the text block.

hDC = CreateIC(TEXT("display"), NULL, NULL, NULL);
SelectObject(hDC, g_hfntMessage);
SetRect(&rect, 0, 0, SIZE_TBWIDTH, 0);
DrawText(hDC, psz, -1, &rect, DT_CALCRECT | DT_WORDBREAK);
DeleteDC(hDC);

ptb->h = (SHORT)(rect.bottom - rect.top);

>>> Objective:
To fit a 4:3 rectangle into the center of another rectangle.

>>> Solution:

// assume rcBig.left = rcBig.top = 0

if (rcBig.right * 3 > rcBig.bottom * 4)
{
    // rcBig is a horizontal bar, fit its height

    dy = rcBig.bottom;
    dx = dy * 4 / 3;
    dt = (rcBig.right - dx) / 2;

    SetRect(&rcSmall, dt, 0, dt + dx, dy);
}
else
{
    // rcBig is a vertical bar, fit its width

    dx = rcBig.right;
    dy = dx * 3 / 4;
    dt = (rcBig.bottom - dy) / 2;

    SetRect(&rcSmall, 0, dt, dx, dt + dy);
}

>>> Objective:
To determine whether the target is hit.

>>> Solution:
We know the target is actually a rectangle, in fact the beam is a rectangle too. So if the intersection of the two rectangles is an empty set, the target is not hit. This method is directly but less efficient, I used this method in this game.

BOOL SDamageJudgment(PBEAM pb, PRECT prc)
{
    int x, y;
    RECT rcAtk, rc;

    // ...

    x = pb->x + (SIZE_PLAYERBIG - SIZE_QUAD) / 2;
    y = pb->y + (SIZE_PLAYERBIG - SIZE_QUAD) / 2;

    SetRect(&rcAtk, x, 0, x + SIZE_QUAD, SIZE_LOGICY);
    IntersectRect(&rc, &rcAtk, prc);

    if (IsRectEmpty(&rc));
    else
        return TRUE;

    // ...

    return FALSE;
}

>>> Objective:
To bounce the moving object when it reaches the edge.

>>> Solution:
We know in this game we have two types of moving object, one is Bicho (ball), another is player. Bicho must be bounced while player does not. Bounce is divided into two parts: horizontally and vertically.

BOOL SMoveIt(PRECT prcObj, CONST PRECT prcFrame, PSHORT pdx, PSHORT pdy, BOOL bPlayer)
{
    RECT rect;
    BOOL bRet;

    bRet = FALSE;
    OffsetRect(prcObj, *pdx, *pdy);
    IntersectRect(&rect, prcObj, prcFrame);

    if (EqualRect(&rect, prcObj)); // has not reached the edge yet
    else
    {
         // bounce horizontally
        if (*pdx < 0)
        {
            if (prcObj->left < prcFrame->left) // out-of-bounds in left side
            {
                if (bPlayer)
                    OffsetRect(prcObj, prcFrame->left - prcObj->left, 0);
                else
                {
                    OffsetRect(prcObj, (prcFrame->left - prcObj->left) * 2, 0);
                    *pdx = -*pdx;
                }
                bRet = TRUE;
            }
        }
        else
        {
            if (prcObj->right > prcFrame->right) // out-of-bounds in right side
            {
                if (bPlayer)
                    OffsetRect(prcObj, prcFrame->right - prcObj->right, 0);
                else
                {
                    OffsetRect(prcObj, (prcFrame->right - prcObj->right) * 2, 0);
                    *pdx = -*pdx;
                }
                bRet = TRUE;
            }
        }

        // bounce vertically
        if (*pdy < 0)
        {
            if (prcObj->top < prcFrame->top) // out-of-bounds in top
            {
                if (bPlayer)
                    OffsetRect(prcObj, 0, prcFrame->top - prcObj->top);
                else
                {
                    OffsetRect(prcObj, 0, (prcFrame->top - prcObj->top) * 2);
                    *pdy = -*pdy;
                }
            }
        }
        else
        {
            if (prcObj->bottom > prcFrame->bottom) // out-of-bounds in bottom
            {
                if (bPlayer)
                    OffsetRect(prcObj, 0, prcFrame->bottom - prcObj->bottom);
                else
                {
                    OffsetRect(prcObj, 0, (prcFrame->bottom - prcObj->bottom) * 2);
                    *pdy = -*pdy;
                }
            }
        }
    }

    return bRet;
}

Sockets

Server maintains six TCP sockets and one UDP socket, in g_sTcp[6] and g_sUdp, respectively. g_sTcp[0] is listening socket, the other fives are communication sockets, they were placed in a single array, but they have different usage.

UDP socket is not very useful in this game because it can not send UDP packs to other client that in a subnet. In this game we handle two UDP messages:

1. UPH_GREETING, client reports its address to server.
2. UPH_MSG, text messages between players.

Most communications are transmitted through TCP socket, this kind of socket is stream oriented. Since we use it to transmit various of informations, we must bundle our messages into packs. The pack always has a head:

typedef struct tagTCPPACK
{
    BYTE    msgtype;
    BYTE    to;
    WORD    msglen;
} TCPPACK, *PTCPPACK;

and follows a block of data. When reading, we must first read sizeof(TCPPACK) bytes from byte stream, then decide how many bytes we need to read next.

On the other hand, we used WSAAsyncSelect(..., FD_READ | FD_WRITE), this will generate a message with FD_READ whenever there is data available in our TCP buffer, so the first read will generate another FD_READ, if msglen is not equal to zero. We then read msglen bytes from TCP buffer and possibly extract all dates out. When finish, we must handle another FD_READ with nothing to read, this is not an error. In the game the code is like this:

// ...
case GM_TCP4:
case GM_TCP5:
    wError  = WSAGETSELECTERROR(lParam);
    wEvent  = WSAGETSELECTEVENT(lParam);
    i       = uMsg - GM_TCP0;

    switch (wEvent)
    {
    case FD_READ:
        if (wError)
            SRemovePlayer(i);
        else
        {
            // results in another FD_READ
            nRead = recv(g_sTcp[i], pbuf, sizeof(TCPPACK), 0);

            if (nRead == SOCKET_ERROR)
            {
                // the "another FD_READ"
            }
            else
            {
                switch (((PTCPPACK)pbuf)->msgtype)
                {
                case TPH_DE1:
                    recv(g_sTcp[i], pbuf, sizeof(XCDE1), 0);
                    // ...
                    break;
                }
            }
        }
        break;
    }
    return 0;

a sample on sending and receiving data

to send data, we must combine two parts together, a head and a data block. the simplest way is define a structure for data block, this way the msglen field isn't relly needed:

// typedef struct tagSTATECHANGE
// {
//     BYTE    id;
//     BYTE    state;
//     BYTE    life;
//     BYTE    pad;
//     WORD    cx;
//     WORD    cy;
// } STATECHANGE, *PSTATECHANGE;

// CHAR pbuf[1024];

case GM_STATECHANGE:
    ((PTCPPACK)pbuf)->msgtype   = TPH_STATECHANGE;
    ((PTCPPACK)pbuf)->to        = 0;
    ((PTCPPACK)pbuf)->msglen    = sizeof(STATECHANGE);

    ((PSTATECHANGE)((PTCPPACK)pbuf + 1))->id    = (BYTE)lParam;
    ((PSTATECHANGE)((PTCPPACK)pbuf + 1))->state = (BYTE)wParam;
    ((PSTATECHANGE)((PTCPPACK)pbuf + 1))->life  = g_vPlayers.at(lParam).life;
    ((PSTATECHANGE)((PTCPPACK)pbuf + 1))->cx    = (WORD)g_vPlayers.at(lParam).rc.left;
    ((PSTATECHANGE)((PTCPPACK)pbuf + 1))->cy    = (WORD)g_vPlayers.at(lParam).rc.top;

    SPushAll((PTCPPACK)pbuf);
    return 0;

msglen is useful when it's a variable, this case we code like below:

char pbuf[1024];
int i, j, n;

#define X ((PBALLCOREINFO)((PTCPPACK)pbuf + 1) + j)

j = 0;

n = g_vpbBlue.size();
for (i = 0; i < n; ++i)
{
    if (g_vpbBlue.at(i)->state)
    {
        X->color    = g_vpbBlue.at(i)->dx > 0 ? ICON_BLUE1 : ICON_BLUE2;
        X->dx       = (signed char)g_vpbBlue.at(i)->dx;
        X->dy       = (signed char)g_vpbBlue.at(i)->dy;
        X->cx       = (WORD)g_vpbBlue.at(i)->rc.left;
        X->cy       = (WORD)g_vpbBlue.at(i)->rc.top;
    }
    else
    {
        X->color    = ICON_PWNED1;
        X->dx       = 0;
        X->dy       = 0;
        X->cx       = (WORD)g_vpbBlue.at(i)->rc.left;
        X->cy       = (WORD)g_vpbBlue.at(i)->rc.top;
    }

    ++j;
}

// ...

#undef X

// we do not know the value of msglen until now

((PTCPPACK)pbuf)->msgtype   = TPH_SYNBALL;
((PTCPPACK)pbuf)->to        = (BYTE)nIndex;
((PTCPPACK)pbuf)->msglen    = (WORD)(j * sizeof(BALLCOREINFO));

if (toall)
    SPushAll((PTCPPACK)pbuf);
else
    SPush((PTCPPACK)pbuf, nIndex);

now msglen is a variable. We then read them.

Read the first message is simple, because we know it's a structure:

case TPH_STATECHANGE:
    nRead = recv(g_sTcp[0], pbuf, ((PTCPPACK)pbuf)->msglen, 0);
    CPlacePlayer((PSTATECHANGE)pbuf);
    break;

CPlacePlayer is a function that takes a pointer of STATECHANGE structure.

Read the second message is relatively complex, we need to use the msglen field.

// nRead = recv(g_sTcp[0], pbuf, sizeof(TCPPACK), 0);

case TPH_SYNBALL:
    if (HeapSize(g_hHeap, 0, g_pbci) <
        ((PTCPPACK)pbuf)->msglen + sizeof(BALLCOREINFO))
    {
        HeapFree(g_hHeap, 0, g_pbci);
        g_pbci = (PBALLCOREINFO)HeapAlloc(g_hHeap, 0,
            ((PTCPPACK)pbuf)->msglen + sizeof(BALLCOREINFO));
    }

    nRead = recv(g_sTcp[0], (PCHAR)g_pbci, ((PTCPPACK)pbuf)->msglen, 0);

    // msglen = 0 so nRead is SOCKET_ERROR, lasterror = WSAEWOULDBLOCK
    if (nRead == SOCKET_ERROR)
        g_pbci->color = ICON_ENDLIST;
    else
    {
        nRead /= sizeof(BALLCOREINFO);
        (g_pbci + nRead)->color = ICON_ENDLIST;
    }
    break;

This time we copy the data block part into another byte block (not pbuf), the head is already extract so in this read we begin from the beginning of the data block.

The Details

The details are laid in source code :D download and check it.

====================
HOW TO RUN
====================

this application is designed to running in Windows XP operating system,
it may not run in other operating system.

if you see this dialog in Windows XP:

" This application has failed to start because
the application configuration is incorrect "

this is because the application requires some dlls in your WinSxS folder. 
please install "vcredist_x86.exe" to try to fix this issue.

====================
HOW TO PLAY
====================

===== whats on the first dialog box =====

select a game mode when hosting a game
                      enter ip address when joining a game
   ------ game mode ------         ------------
  | Cooperative           |       | ip address |
  | Free for all          |        ------------
  | Team Match            |
  | Duel                  |         ------
   -----------------------         | name |        ------
                                    ------        | icon |
                                    ------         ------
if server specify its port,        | port |       -------
client need specify the same        ------       | color |
                                                  -------
      -------------    -------------    -------------
     | Host button |  | Join button |  | Quit button |
      -------------    -------------    -------------


===== when hosting a game =====

you must
- click "Host" button

you can
- select a game mode, default to FAIR
- specify a port number, default to 666
- specify your name, default to Bicho
- choose an icon, default to Cross
- choose a color, default to Red
- click "Quit" button, if you want to quit

===== when joining a game =====

you must
- fullfill the (hosts, not yours) ip address area
- click "Join" button

you can
- specify port number, default to 666
- specify your name, default to Bicho
- choose an icon, default to Cross
- choose a color, default to Red
- click "Quit" button, if you want to quit

===== whats on the in-game menu dialog box =====

  join
  resume
  back to main menu
  back to desktop

===== controls =====

in this game you have 7 keyboard keys and 2 mouse buttons to use.

   keys: 4 arrow keys, Esc, Enter, Spacebar
buttons: left button, right button (zoom in and out)

===== objective =====

This game include four modes:

1. Cooperative
2. Free for all (PVP)
3. Team Match (PVP)
4. Duel (PVP)

Press spacebar to fire, the effect is two beams from the center
of your icon. Arrow keys to move your icon.

In PVP mode, you should try to let your beam hit other players
and avoid be hit. Hit moving balls (called Bicho) may enhance
your ability (speed etc.).

COOP level 1:
COOP level 2:
Eliminate all Bichos.
If hit nothing, minus one life.

COOP level 3:
COOP level 4:
Eliminate all Bichos within one hit.
Otherwise, minus one life, map restart.
If hit nothing, minus one life.

COOP level 5:
Eliminate all Bichos within two hits.
Otherwise, minus one life, map restart.
If hit nothing, minus one life.

COOP level 6:
Eliminate all three red Bichos within one hit, do not hit others.
Otherwise, minus one life, map restart.

COOP level 7:
Execute the traitor, do not hit others.
Otherwise, minus one life, map restart.

COOP level 8:
Eliminate all Bichos within two hits.
Otherwise, minus one life, map restart.
If hit nothing, minus one life.

COOP level 9:
Eliminate all fireballs.

COOP level 10:
Eliminate all skulls.
Your normal attack can freeze those skulls for ever.

FAIR:
Gain 20 kills.

TEAM:
Eliminate all your enemies.
Only reds and blues are allowed to join.

DUEL:
Eliminate your opponent.

===== ===== ===== =====
my email: johns@sina.com
===== ===== ===== =====

History

2010.02.19
Added a zip file: src_en_string.zip. It contains four files:
Dialogs.cpp
GameEntry.cpp
Hud.cpp
Resource.rc
These files print English texts instead of Chinese texts, you can use them to replace the files in original zip(src.zip).
Map.cpp contains lots of Chinese texts but all of them are hint texts. You can find these hints' English version in HowTo.txt.
==========

2010.02.18
vcredist_x86.exe was removed.
Bicho.exe was moved to another .zip file, executable.zip
==========

Gadgets.cpp is removed from this project. I mention it here because it's meaningful for me but is meaningless for this game.

Countdown box is not implemented. It's intended to be used in a duel game.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionConverting non multiplayer game into multiplayer game Pin
swdev.bali24-Mar-12 0:23
swdev.bali24-Mar-12 0:23 
Joke;) [modified] Pin
lusores17-Feb-10 11:01
lusores17-Feb-10 11:01 
AnswerRe: ;) Pin
see1see17-Feb-10 23:13
see1see17-Feb-10 23:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.