Fba 街机对战实现


Fba源码分析

前面一篇文章中分析了FBa 中引入开源的Kaillera库,可以实现街机的对战,简要的介绍了他对应的功能,也从对应的网站上下载到了开源的代码,客户端以及服务端,测试是可以使用的,下面就简要的分析 下,这个对战库在Fba 源码中是怎么样使用的,这样才能写出对应的测试代码,来调试分析对战库

前面已经分析过了,主函数的入口位置为 src/burn/win32/main.cpp文件,下面就大致的介绍看下对应的源码

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR lpCmdLine, int nShowCmd)
{
    ...
    if (!(AppInit())) {                            // Init the application
        if (bAlwaysCreateSupportFolders) CreateSupportFolders();
        if (!(ProcessCmdLine())) {
            DetectWindowsVersion();
            EnableHighResolutionTiming();

            MediaInit();

            RunMessageLoop();                    // Run the application message loop
        }
    }
    ...
}

首先看AppInit 函数的实现
static int AppInit()
{
    ...
    // Init the Burn library  
    BurnLibInit();
    ...
}

extern "C" INT32 BurnLibInit()
{
    BurnLibExit();
    nBurnDrvCount = sizeof(pDriver) / sizeof(pDriver[0]);    // count available drivers

    cmc_4p_Precalc();
    bBurnUseMMX = BurnCheckMMXSupport();

    return 0;
}

这里主要是算出当前街机支持的drivers的数量,是通过 sizeof(pDriver) / sizeof(pDriver[0]),这里的pDriver 为
// Structure containing addresses of all drivers
// Needs to be kept sorted (using the full game name as the key) to prevent problems with the gamelist in Kaillera
static struct BurnDriver* pDriver[] = {
    &BurnDrvgames88,            // '88 Games
    &BurnDrvFlagrall,            // '96 Flag Rally
    &BurnDrv99lstwar,            // '99: The Last War
    &BurnDrv99lstwara,            // '99: The Last War (alternate)
    &BurnDrv99lstwark,            // '99: The Last War (Kyugo)
    &BurnDrvMSX_007tld,            // 007 - The Living Daylights (Euro)
    &BurnDrvsg1k_jb007a,        // 007 James Bond (Jpn, v2.6, OMV)
    &BurnDrvsg1k_jb007,            // 007 James Bond (Jpn, v2.7, OMV)
    &BurnDrvsg1k_jb007t,        // 007 James Bond (Tw)
    &BurnDrvMSX_10yard,            // 10-Yard Fight (Jpn)
    &BurnDrvMSX_10yarda,        // 10-Yard Fight (Jpn, Alt)
    &BurnDrvGtmro,                // 1000 Miglia: Great 1000 Miles Rally (94/05/10)
    &BurnDrvGtmrb,                // 1000 Miglia: Great 1000 Miles Rally (94/05/26)
    &BurnDrvGtmra,                // 1000 Miglia: Great 1000 Miles Rally (94/06/13)
    &BurnDrvGtmr,                // 1000 Miglia: Great 1000 Miles Rally (94/07/18)
    &BurnDrvmd_12in1,            // 12 in 1
    &BurnDrvmd_13mahjan,        // 13 Ma Jiang - 98 Mei Shao Nu Pian (Chi)
    &BurnDrvmd_16tongnk,        // 16 Ton (Jpn, Game no Kandume MegaCD Rip)
    &BurnDrvmd_16ton,            // 16 Ton (Jpn, SegaNet)
    &BurnDrvmd_16zhan,            // 16 Zhang Ma Jiang (Chi)
    &BurnDrvMSX_180,            // 180 Degrees
.....
}

其中的BurnDriver 结构体代表引擎的通用结构体
struct BurnDriver {
    char* szShortName;            // The filename of the zip file (without extension)
    char* szParent;                // The filename of the parent (without extension, NULL if not applicable)
    char* szBoardROM;            // The filename of the board ROMs (without extension, NULL if not applicable)
    char* szSampleName;            // The filename of the samples zip file (without extension, NULL if not applicable)
    char* szDate;

    // szFullNameA, szCommentA, szManufacturerA and szSystemA should always contain valid info
    // szFullNameW, szCommentW, szManufacturerW and szSystemW should be used only if characters or scripts are needed that ASCII can't handle
    char*    szFullNameA; char*    szCommentA; char*    szManufacturerA; char*    szSystemA;
    wchar_t* szFullNameW; wchar_t* szCommentW; wchar_t* szManufacturerW; wchar_t* szSystemW;

    INT32 Flags;            // See burn.h
    INT32 Players;        // Max number of players a game supports (so we can remove single player games from netplay)
    INT32 Hardware;        // Which type of hardware the game runs on
    INT32 Genre;
    INT32 Family;
    INT32 (*GetZipName)(char** pszName, UINT32 i);                // Function to get possible zip names
    INT32 (*GetRomInfo)(struct BurnRomInfo* pri, UINT32 i);        // Function to get the length and crc of each rom
    INT32 (*GetRomName)(char** pszName, UINT32 i, INT32 nAka);    // Function to get the possible names for each rom
    INT32 (*GetSampleInfo)(struct BurnSampleInfo* pri, UINT32 i);        // Function to get the sample flags
    INT32 (*GetSampleName)(char** pszName, UINT32 i, INT32 nAka);    // Function to get the possible names for each sample
    INT32 (*GetInputInfo)(struct BurnInputInfo* pii, UINT32 i);    // Function to get the input info for the game
    INT32 (*GetDIPInfo)(struct BurnDIPInfo* pdi, UINT32 i);        // Function to get the input info for the game
    INT32 (*Init)(); INT32 (*Exit)(); INT32 (*Frame)(); INT32 (*Redraw)(); INT32 (*AreaScan)(INT32 nAction, INT32* pnMin);
    UINT8* pRecalcPal; UINT32 nPaletteEntries;                                        // Set to 1 if the palette needs to be fully re-calculated
    INT32 nWidth, nHeight; INT32 nXAspect, nYAspect;                    // Screen width, height, x/y aspect
};
比如,当前引擎的名称,支持多少个玩家,游戏的宽高信息等,以及对应的一些函数指针,比如初始化,退出,界面的渲染,获取按键的信息获取rom的信息等 ,这样不同的引擎就要实现对应的函数指针的实现,

这里我们看一个BurnDrvgames88 
struct BurnDriver BurnDrvgames88 = {
    "88games", NULL, NULL, NULL, "1988",
    "'88 Games\0", NULL, "Konami", "GX861",
    NULL, NULL, NULL, NULL,
    BDF_GAME_WORKING, 4, HARDWARE_PREFIX_KONAMI, GBF_SPORTSMISC, 0,
    NULL, games88RomInfo, games88RomName, NULL, NULL, games88InputInfo, games88DIPInfo,
    DrvInit, DrvExit, DrvFrame, DrvDraw, DrvScan, &DrvRecalc, 0x800,
    304, 224, 4, 3
};
大致就是这个引擎的名称为88games,最多支持的玩家数量为4个,游戏的分辨率,已经对应的函数指针,所以BurnLibInit 列出了当前支持的引擎数量

接着main函数继续执行 ,执行到 MediaInit(); 函数

int MediaInit()
{
    if (ScrnInit()) {                    // Init the Scrn Window
        FBAPopupAddText(PUF_TEXT_DEFAULT, MAKEINTRESOURCE(IDS_ERR_UI_WINDOW));
        FBAPopupDisplay(PUF_TYPE_ERROR);
        return 1;
    }

    if (!bInputOkay) {
        InputInit();                    // Init Input
    }

    nAppVirtualFps = nBurnFPS;

    if (!bAudOkay) {
        AudSoundInit();                    // Init Sound (not critical if it fails)
    }

    nBurnSoundRate = 0;                    // Assume no sound
    pBurnSoundOut = NULL;
    if (bAudOkay) {
        nBurnSoundRate = nAudSampleRate[nAudSelect];
        nBurnSoundLen = nAudSegLen;
    }

    if (!bVidOkay) {

        // Reinit the video plugin
        VidInit();
        if (!bVidOkay && nVidFullscreen) {

            nVidFullscreen = 0;

            MediaExit();
            return (MediaInit());
        }
        if (!nVidFullscreen) {
            ScrnSize();
        }

        if (!bVidOkay) {
            // Make sure the error will be visible
            SplashDestroy(1);

            FBAPopupAddText(PUF_TEXT_DEFAULT, MAKEINTRESOURCE(IDS_ERR_UI_VID_MODULE), VidGetModuleName());
            FBAPopupDisplay(PUF_TYPE_ERROR);
        }

        if (bVidOkay && ((bRunPause && bAltPause) || !bDrvOkay)) {
            VidRedraw();
        }
    }
    return 0;
}

首先初始化窗口,设置窗口对应的事件等
// Init the screen window (create it)
int ScrnInit()
{
    ...
    if (ScrnRegister() != 0) {
        return 1;
    }
    ....
    hScrnWnd = CreateWindowEx(nWindowExStyles, szClass, _T(APP_TITLE), nWindowStyles,
        0, 0, 0, 0,                                                   // size of window
        NULL, NULL, hAppInst, NULL);
    ....    
}
上面的代码主要是创建对应的显示窗口,并且设置对应的事件触发函数 ScrnRegister 函数,执行窗口的注册,包括事件等
static int ScrnRegister()
{
    WNDCLASSEX WndClassEx;
    ATOM Atom = 0;

    // Register the window class
    memset(&WndClassEx, 0, sizeof(WndClassEx));         // Init structure to all zeros
    WndClassEx.cbSize            = sizeof(WndClassEx);
    WndClassEx.style            = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS | CS_CLASSDC;// These cause flicker in the toolbar
    WndClassEx.lpfnWndProc        = ScrnProc;
    WndClassEx.hInstance        = hAppInst;
    WndClassEx.hIcon            = LoadIcon(hAppInst, MAKEINTRESOURCE(IDI_APP));
    WndClassEx.hCursor            = LoadCursor(NULL, IDC_ARROW);
    WndClassEx.hbrBackground    = static_cast<HBRUSH>( GetStockObject ( BLACK_BRUSH ));
    WndClassEx.lpszClassName    = szClass;

    // Register the window class with the above information:
    Atom = RegisterClassEx(&WndClassEx);
    if (Atom) {
        return 0;
    } else {
        return 1;
    }
}

窗口的注册事件主要是通过 RegisterClassEx 实现的,具体可以查对应的api,  lpfnWndProc:代表窗口处理函数的指针。,也即是对应的ScrnProc 函数
static LRESULT CALLBACK ScrnProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
    ...
    switch (Msg) {
        HANDLE_MSG(hWnd, WM_CREATE,            OnCreate);
        //HANDLE_MSG(hWnd, WM_ACTIVATEAPP,    OnActivateApp);
        HANDLE_MSGB(hWnd,WM_PAINT,            OnPaint);
        HANDLE_MSG(hWnd, WM_CLOSE,            OnClose);
        HANDLE_MSG(hWnd, WM_DESTROY,        OnDestroy);
        HANDLE_MSG(hWnd, WM_COMMAND,        OnCommand);
    ...
}
HANDLE_MSG是 Win32应用中的回调函数WndProc用于接收Windows向应用程序直接发送的消息,以及响应消息,这些事件对应的也即是窗口创建的时候,执行函数OnCreate,窗口绘制的时候,执行OnPaint,
窗口销毁的时候,执行OnDestroy 等,这里我们的窗口按钮的点击是在OnCommand 函数实现的,具体的窗口的绘制就不看了,都是利用window下特有的api实现的,这里主要看按钮的事件响应函数

static void OnCommand(HWND /*hDlg*/, int id, HWND /*hwndCtl*/, UINT codeNotify)
{
        case MENU_LOAD://普通的单机选择游戏事件
        {
            .....
            nGame = SelDialog(0, hScrnWnd);        // Bring up select dialog to pick a driver

            extern bool bDialogCancel;

            if (nGame >= 0 && bDialogCancel == false) {
                DrvExit();
                DrvInit(nGame, true);            // Init the game driver
                .....
                break;
            } 
            ....
        }
        case MENU_STARTNET:  //网络对战的按钮事件
        {
            if (Init_Network()) {
                MessageBox(hScrnWnd, FBALoadStringEx(hAppInst, IDS_ERR_NO_NETPLAYDLL, true), FBALoadStringEx(hAppInst, IDS_ERR_ERROR, true), MB_OK);
                break;
            }

            if (!kNetGame) {
                InputSetCooperativeLevel(false, bAlwaysProcessKeyboardInput);
                AudBlankSound();
                SplashDestroy(1);
                StopReplay();
                DrvExit();
                DoNetGame();
                MenuEnableItems();
                InputSetCooperativeLevel(false, bAlwaysProcessKeyboardInput);
            }
            break;
        }
    .....    
    }
}

接着继续分析 MediaInit函数 InputInit() 也即是输入的初始化 
INT32 InputInit()
{
    INT32 nRet;

    bInputOkay = false;

    if (nInputSelect >= INPUT_LEN) {
        return 1;
    }

    if ((nRet = pInputInOut[nInputSelect]->Init()) == 0) {
        bInputOkay = true;
    }

    return nRet;
}

这里的pInputInOut 为
static struct InputInOut *pInputInOut[]=
{
#if defined (BUILD_WIN32)
    &InputInOutDInput,
#elif defined (BUILD_SDL)
    &InputInOutSDL,
#elif defined (_XBOX)
    &InputInOutXInput2,
#elif defined (BUILD_QT)
    &InputInOutQt,
#endif
};

可以看出这里提供了对应的平台下的实现,这里是win32 所以这个函数返回 InputInOutDInput,而这个结构体变量的定义为
struct InputInOut InputInOutDInput = { init, exit, setCooperativeLevel, newFrame, getState, readGamepadAxis, readMouseAxis, find, getControlName, NULL, _T("DirectInput8 input") }; 
所以执行到了对应的init,这是一个函数指针
int init()
{
    ...
    if (FAILED(_DirectInput8Create(hAppInst, DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&pDI, NULL))) {
        return 1;
    }

    // keyboard
    if (FAILED(pDI->CreateDevice(GUID_SysKeyboard, &keyboardProperties[0].lpdid, NULL))) {
        return 1;
    }
    ...
}

所以对于MediaInit函数 中的AudSoundInit 函数,这个函数主要完成声音的初始化,跟上面的执行逻辑是一样的
INT32 AudSoundInit()
{
    INT32 nRet;

    if (nAudSelect >= AUD_LEN) {
        return 1;
    }

    nAudActive = nAudSelect;

    if ((nRet = pAudOut[nAudActive]->SoundInit()) == 0) {
        bAudOkay = true;
    }

    return nRet;
}

MediaInit函数 中的VidInit(); 函数,这个主要完成视频的初始化
INT32 VidInit()
{
    ....
    pVidOut[nVidActive]->Init() 
    ...
}
static struct VidOut *pVidOut[] = {
#if defined (BUILD_WIN32)
    &VidOutDDraw,
    &VidOutD3D,
    &VidOutDDrawFX,
    &VidOutDX9,
    &VidOutDX9Alt,
#elif defined (BUILD_SDL)
    &VidOutSDLOpenGL,
    &VidOutSDLFX,
#elif defined (_XBOX)
    &VidOutD3D,
#elif defined (BUILD_QT)
    &VidOutOGL,
#endif
};
所以之类的初始化过程,跟上面也是类似的

接着回到主函数的 RunMessageLoop();                    // Run the application message loop 
// The main message loop
int RunMessageLoop()
{
    int bRestartVideo;
    MSG Msg;

    do {
        ...
        //显示窗口
        ShowWindow(hScrnWnd, nAppShowCmd);                                    
        ....
        //一直循环,用来响应各种事件
        while (1) {
        if (PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE)) {
            // A message is waiting to be processed
            if (Msg.message == WM_QUIT)    {                                            // Quit program
                break;
            }
            if (Msg.message == (WM_APP + 0)) {                                        // Restart video
                bRestartVideo = 1;
                break;
            }
            .....
        }
        else {
            //没有事件,默认执行这里
            bRunPause ? wav_pause(false) : wav_pause(true);
            // No messages are waiting
            SplashDestroy(0);
            RunIdle();
        }
    }
}
PeekMessage是一个Windows API函数。该函数为一个消息检查线程消息队列,并将该消息(如果存在)放于指定的结构。 由于这里是刚初始化,所以没有事件,调用ShowWindow(hScrnWnd, nAppShowCmd); 
那么默认的窗口也就显示出来了,如果没有事件的化,默认执行 RunIdle ,RunIdle 会执行 RunFrame来更新界面

// With or without sound, run one frame.
// If bDraw is true, it's the last frame before we are up to date, and so we should draw the screen
static int RunFrame(int bDraw, int bPause)
{
    static int bPrevPause = 0;
    static int bPrevDraw = 0;

    if (bPrevDraw && !bPause) {
        VidPaint(0);                            // paint the screen (no need to validate)
    }

    if (!bDrvOkay) {//由于我们还没有加载对应的引擎,所以会执行到这里
        return 1;
    }
    .....
}

上面就是显示默认的流程,现在点击加载游戏,查看对应的流程,也即是上面分析的onCommand中的 第一个case语句
static void OnCommand(HWND /*hDlg*/, int id, HWND /*hwndCtl*/, UINT codeNotify)
{
    if (bLoading) {
        return;
    }

    switch (id) {
        case MENU_LOAD://普通的单机选择游戏事件
        {
            ...
            nGame = SelDialog(0, hScrnWnd);        // Bring up select dialog to pick a driver

            extern bool bDialogCancel;

            if (nGame >= 0 && bDialogCancel == false) {
                DrvExit();
                DrvInit(nGame, true);            // Init the game driver
                .....
                break;
            } 
            ...
        }
        ...
    }
    ...
}

首先显示选择游戏的对话框     nGame = SelDialog(0, hScrnWnd); 当游戏关闭的时候,会返回选择的游戏对应的索引
DrvInit(nGame, true);初始化对应的引擎

int DrvInit(int nDrvNum, bool bRestore)
{
    ...
    DrvExit();                        // 确保退出
    MediaExit();

    nBurnDrvActive = nDrvNum;        // Set the driver number      保存当前下载的游戏引擎对应的下标索引

    ...

    // Define nMaxPlayers early; GameInpInit() needs it (normally defined in DoLibInit()).
    nMaxPlayers = BurnDrvGetMaxPlayers();
    GameInpInit();                    // Init game input
    ...

    bDrvOkay = 1;                        // Okay to use all BurnDrv functions   初始化完成之后,标识引擎准备好了
    ...
}

BurnDrvGetMaxPlayers(); 获取到当前游戏引擎支持的游戏玩家数量,其实很简单,就从支持的引擎结构体数组中,获取到对应的引擎结构体,然后获取到对应的成员
extern "C" INT32 BurnDrvGetMaxPlayers()
{
    return pDriver[nBurnDrvActive]->Players;
}

GameInpInit();执行初始化游戏按键信息
INT32 GameInpInit()
{
    // Count the number of inputs  计算输入按键的数量
    nGameInpCount = 0;
    nMacroCount = 0;
    nMaxMacro = nMaxPlayers * 52;

    for (UINT32 i = 0; i < 0x1000; i++) {
        nRet = BurnDrvGetInputInfo(NULL,i);
        if (nRet) {                                                        // end of input list
            nGameInpCount = i;
            break;
        }
    }

    // Allocate space for all the inputs  给当前游戏最多支持输入按键分配内存空间
    INT32 nSize = (nGameInpCount + nMaxMacro) * sizeof(struct GameInp);
    GameInp = (struct GameInp*)malloc(nSize);
    if (GameInp == NULL) {
        return 1;
    }
    memset(GameInp, 0, nSize);

    //给上面分配的内存按键赋值操作,赋值引擎按键的真正的内容
    GameInpBlank(1);
    ...
}

nRet = BurnDrvGetInputInfo(NULL,i); 获取到按键的信息
extern "C" INT32 BurnDrvGetInputInfo(struct BurnInputInfo* pii, UINT32 i)    // Forward to drivers function
{
    return pDriver[nBurnDrvActive]->GetInputInfo(pii, i);
}
也即是对应的引擎结构体数组中,获取到对应的引擎结构体,然后获取到对应的成员,这里假设当前的引擎结构体为
struct BurnDriver BurnDrvgames88 = {
    "88games", NULL, NULL, NULL, "1988",
    "'88 Games\0", NULL, "Konami", "GX861",
    NULL, NULL, NULL, NULL,
    BDF_GAME_WORKING, 4, HARDWARE_PREFIX_KONAMI, GBF_SPORTSMISC, 0,
    NULL, games88RomInfo, games88RomName, NULL, NULL, games88InputInfo, games88DIPInfo,
    DrvInit, DrvExit, DrvFrame, DrvDraw, DrvScan, &DrvRecalc, 0x800,
    304, 224, 4, 3
};
那么这个成员对应的也即是games88InputInfo函数指针,这个函数指针的定义是在宏里面定义的 也即是 STDINPUTINFO(games88) 下面是这个宏的具体实现 宏里面##代表字符串的拼接
#define STDINPUTINFO(Name)                                                \
static INT32 Name##InputInfo(struct BurnInputInfo* pii, UINT32 i)        \
{                                                                        \
    if (i >= sizeof(Name##InputList) / sizeof(Name##InputList[0])) {    \
        return 1;                                                        \
    }                                                                    \
    if (pii) {                                                            \
        *pii = Name##InputList[i];                                        \
    }                                                                    \
    return 0;                                                            \
}
所以宏STDINPUTINFO(games88) 展开大致是这样的 
static INT32 games88InputInfo(struct BurnInputInfo* pii, UINT32 i)        
{                                                                        
    if (i >= sizeof(games88InputList) / sizeof(games88InputList[0])) {    
        return 1;                                                        
    }                                                                    
    if (pii) {                                                            
        *pii = games88InputList[i];                                        
    }                                                                    
    return 0;                                                            
}
而games88InputList 的定义为,其他的函数指针也是大致的逻辑
static struct BurnInputInfo games88InputList[] = {
    {"P1 Coin",        BIT_DIGITAL,    DrvJoy1 + 0,    "p1 coin"    },
    {"P1 Start",        BIT_DIGITAL,    DrvJoy2 + 3,    "p1 start"    },
    {"P1 Button 1",        BIT_DIGITAL,    DrvJoy2 + 0,    "p1 fire 1"    },
    {"P1 Button 2",        BIT_DIGITAL,    DrvJoy2 + 1,    "p1 fire 2"    },
    {"P1 Button 3",        BIT_DIGITAL,    DrvJoy2 + 2,    "p1 fire 3"    },

    {"P2 Coin",        BIT_DIGITAL,    DrvJoy1 + 1,    "p2 coin"    },
    {"P2 Start",        BIT_DIGITAL,    DrvJoy2 + 7,    "p2 start"    },
    {"P2 Button 1",        BIT_DIGITAL,    DrvJoy2 + 4,    "p2 fire 1"    },
    {"P2 Button 2",        BIT_DIGITAL,    DrvJoy2 + 5,    "p2 fire 2"    },
    {"P2 Button 3",        BIT_DIGITAL,    DrvJoy2 + 6,    "p2 fire 3"    },
    ....
};

BurnInputInfo 结构体定义为 
struct BurnInputInfo {
    char* szName;
    UINT8 nType;
    union {
        UINT8* pVal;                    // Most inputs use a char*
        UINT16* pShortVal;                // All analog inputs use a short*
    };
    char* szInfo;
};

DrvJoy1,DrvJoy2,DrvJoy3 本质为一个数组,所以对于上面结构体的第三个成员,类似的DrvJoy1 代表这个数组的首地址 +0或者1等代表指针的偏移,也即是对应的数组的成员
static UINT8 DrvJoy1[8];
static UINT8 DrvJoy2[8];
static UINT8 DrvJoy3[8];
static UINT8 DrvDips[3];
static UINT8 DrvInputs[3];

所以对于DrvJoy1 + 0 代表 DrvJoy1数组的第一个成员的地址  DrvJoy2 + 3代表这个数组的第四个成员的地址

继续分析 GameInpBlank(1); 函数的实现
INT32 GameInpBlank(INT32 bDipSwitch)
{
    UINT32 i = 0;
    struct GameInp* pgi = NULL;

    // Reset all inputs to undefined (even dip switches, if bDipSwitch==1)
    if (GameInp == NULL) {
        return 1;
    }

    //遍历上一步分配的按键的内存数组,完成赋值操作
    // Get the targets in the library for the Input Values
    for (i = 0, pgi = GameInp; i < nGameInpCount; i++, pgi++) {
        struct BurnInputInfo bii;
        memset(&bii, 0, sizeof(bii));
        BurnDrvGetInputInfo(&bii, i);
        if (bDipSwitch == 0 && (bii.nType & BIT_GROUP_CONSTANT)) {        // Don't blank the dip switches
            continue;
        }

        memset(pgi, 0, sizeof(*pgi));                                    // Clear input

        pgi->nType = bii.nType;                                            // store input type                存储按键的类型
        pgi->Input.pVal = bii.pVal;                                        // store input pointer to value  存储按键的指针,方便我们后面修改按键的内容

        if (bii.nType & BIT_GROUP_CONSTANT) {                            // Further initialisation for constants/DIPs
            pgi->nInput = GIT_CONSTANT;
            pgi->Input.Constant.nConst = *bii.pVal;
        }
    }
    ...
    return 0;
}

这里重点注意下 pgi->Input.pVal = bii.pVal;     前面已经分析过了 bii.pVal 代表对应引擎对应的按键的地址,类似于DrvJoy1 + 0 代表 DrvJoy1数组的第一个成员的地址
那么当这个赋值操作完成之后,那么 pgi->Input.pVal 也指向了这个引擎的按键地址,后面当要修改按键的内容的时候,就可以直接修改pgi->Input.pVal中所代表的内容

继续回到 case语句块继续往下执行  nStatus = DoLibInit();    初始化对应的游戏引擎
static int DoLibInit()                    // Do Init of Burn library driver
{
    int nRet = 0;

    //加载引擎对应的zip包
    if (DrvBzipOpen()) {
        return 1;
    }

    if ((BurnDrvGetHardwareCode() & HARDWARE_PUBLIC_MASK) != HARDWARE_SNK_MVS) {
        if (!bQuietLoading) ProgressCreate();
    }

    //执行对应的引擎初始化操作
    nRet = BurnDrvInit();

    //关闭引擎对应的zip包
    BzipClose();

    if (!bQuietLoading) ProgressDestroy();

    if (nRet) {
        return 3;
    } else {
        return 0;
    }
}

// Init game emulation (loading any needed roms)
extern "C" INT32 BurnDrvInit()
{
    ...
    CheatInit();
    HiscoreInit();
    BurnStateInit();
    BurnInitMemoryManager();
    BurnRandomInit();

    //调用对应的引擎初始化,具体就不看了,逻辑就是获取到对应的引擎结构体,执行对应的函数指针
    nReturnValue = pDriver[nBurnDrvActive]->Init();    // Forward to drivers function

    nMaxPlayers = pDriver[nBurnDrvActive]->Players;
    ....
}

引擎初始化成功之后,int DrvInit(int nDrvNum, bool bRestore)函数后面,会将 bDrvOkay = 1; 标识引擎准备好了,那么继续回调前面分析的RumLooperMessage

当再次执行到 RunIdle();的时候,也即是当前没有任何的按键操作的情况下,界面默认的显示的时候,会调用这个方法渲染游戏的界面,最终会调用到RunFrame 

static int RunFrame(int bDraw, int bPause)
{
    static int bPrevPause = 0;
    static int bPrevDraw = 0;

    if (bPrevDraw && !bPause) {
        VidPaint(0);                            // paint the screen (no need to validate)
    }

    if (!bDrvOkay) {//判断是否引擎准备好了,此时已经准备好了
        return 1;
    }

    ....
    GetInput(true);                    // Update inputs  获取到键盘的按键内容

    ... 
    BurnDrvFrame();                 //渲染按键
    ....

}

GetInput(true);    这里先分析下,按键的信息的获取
static int GetInput(bool bCopy)
{
    ...
    InputMake(bCopy);                         // get input
    ...
}
// This will process all PC-side inputs and optionally update the emulated game side.
INT32 InputMake(bool bCopy)
{
    ...
    for (i = 0, pgi = GameInp; i < nGameInpCount; i++, pgi++) {
        if (pgi->Input.pVal == NULL) {
            continue;
        }

        switch (pgi->nInput) {
            ....
            case GIT_SWITCH: {                        // Digital input  默认的按键情况
                INT32 s = CinpState(pgi->Input.Switch.nCode);          判断当前的按键 被按下的情况  ,具体会调用到window 中的 GetDeviceState 获取到对应的状态

                if (pgi->nType & BIT_GROUP_ANALOG) {
                    // Set analog controls to full
                    if (s) {
                        pgi->Input.nVal = 0xFFFF;                   如果被按下,赋值为  0xFFFF;    前面已经分析过,这个值指向的就是引擎的按键内容,所以这个改变会导致引擎的按键发生改变
                    } else {
                        pgi->Input.nVal = 0x0001;                    如果没有被按下 赋值为  0x0001;    
                    }
                    if (bCopy) {
                        *(pgi->Input.pShortVal) = pgi->Input.nVal;
                    }
                } else {
                    // Binary controls
                    if (s) {
                        pgi->Input.nVal = 1;
                    } else {
                        pgi->Input.nVal = 0;
                    }
                    if (bCopy) {
                        *(pgi->Input.pVal) = pgi->Input.nVal;
                    }
                }

                break;
            }
            case GIT_JOYSLIDER:    {                    // Joystick slider      游戏手柄
                INT32 nSlider = pgi->Input.Slider.nSliderValue;
                if (pgi->nType == BIT_ANALOG_REL) {
                    nSlider -= 0x8000;
                    nSlider >>= 4;
                }

                pgi->Input.nVal = (UINT16)nSlider;
                if (bCopy) {
                    *(pgi->Input.pShortVal) = pgi->Input.nVal;
                }
                break;
            }
            case GIT_MOUSEAXIS:                        // Mouse axis            鼠标
                pgi->Input.nVal = (UINT16)(CinpMouseAxis(pgi->Input.MouseAxis.nMouse, pgi->Input.MouseAxis.nAxis) * nAnalogSpeed);
                if (bCopy) {
                    *(pgi->Input.pShortVal) = pgi->Input.nVal;
                }
                break;
            }
            .....
        }
    }
    ....
    return 0;
}

BurnDrvFrame函数的实现,也即是会调用到对应引擎的函数指针,完成渲染
extern "C" INT32 BurnDrvFrame()
{
    CheatApply();                                    // Apply cheats (if any)
    HiscoreApply();
    return pDriver[nBurnDrvActive]->Frame();        // Forward to drivers function
}

由于每次渲染界面都要去获取到对应的按键情况,然后去修改引擎原本的按键内容,执行对应的引擎渲染,那么引擎就能事实的获取到这些按键的情况,这就是大致的流程,当然还有很多细节

对战代码实现

static void OnCommand(HWND /*hDlg*/, int id, HWND /*hwndCtl*/, UINT codeNotify)
{
    ...
    case MENU_STARTNET:
    {
        if (Init_Network()) {//判断加载对战库成功,如果成功,显示对战库的对话框
            MessageBox(hScrnWnd, FBALoadStringEx(hAppInst, IDS_ERR_NO_NETPLAYDLL, true), FBALoadStringEx(hAppInst, IDS_ERR_ERROR, true), MB_OK);
            break;
        }
        ...
        DoNetGame();
        ...
    }
    ...        
}

Init_Network() 会判断当前是否有对战库
int Init_Network(void)
{
//#if defined (_UNICODE)
//    Kaillera_HDLL = LoadLibrary(L"kailleraclient.dll");
//#else
    Kaillera_HDLL = LoadLibrary("kailleraclient.dll");
//#endif

    if (Kaillera_HDLL != NULL)
    {
        Kaillera_Get_Version = (int (WINAPI *)(char *version)) GetProcAddress(Kaillera_HDLL, "_kailleraGetVersion@4");
        Kaillera_Init = (int (WINAPI *)()) GetProcAddress(Kaillera_HDLL, "_kailleraInit@0");
        Kaillera_Shutdown = (int (WINAPI *)()) GetProcAddress(Kaillera_HDLL, "_kailleraShutdown@0");
        Kaillera_Set_Infos = (int (WINAPI *)(kailleraInfos *infos)) GetProcAddress(Kaillera_HDLL, "_kailleraSetInfos@4");
        Kaillera_Select_Server_Dialog = (int (WINAPI *)(HWND parent)) GetProcAddress(Kaillera_HDLL, "_kailleraSelectServerDialog@4");
        Kaillera_Modify_Play_Values = (int (WINAPI *)(void *values, int size)) GetProcAddress(Kaillera_HDLL, "_kailleraModifyPlayValues@8");
        Kaillera_Chat_Send = (int (WINAPI *)(char *text)) GetProcAddress(Kaillera_HDLL, "_kailleraChatSend@4");
        Kaillera_End_Game = (int (WINAPI *)()) GetProcAddress(Kaillera_HDLL, "_kailleraEndGame@0");

        if ((Kaillera_Get_Version != NULL) && (Kaillera_Init != NULL) && (Kaillera_Shutdown != NULL) && (Kaillera_Set_Infos != NULL) && (Kaillera_Select_Server_Dialog != NULL) && (Kaillera_Modify_Play_Values != NULL) && (Kaillera_Chat_Send != NULL) && (Kaillera_End_Game != NULL))
        {            
            //执行了Kaillera_Init()函数,进行库的初始化
            Kaillera_Init();
            Kaillera_Initialised = 1;
            return 0;
        }

        FreeLibrary(Kaillera_HDLL);
    } else {
    }

    Kaillera_Get_Version = Empty_Kaillera_Get_Version;
    Kaillera_Init = Empty_Kaillera_Init;
    Kaillera_Shutdown = Empty_Kaillera_Shutdown;
    Kaillera_Set_Infos = Empty_Kaillera_Set_Infos;
    Kaillera_Select_Server_Dialog = Empty_Kaillera_Select_Server_Dialog;
    Kaillera_Modify_Play_Values = Empty_Kaillera_Modify_Play_Values;
    Kaillera_Chat_Send = Empty_Kaillera_Chat_Send;
    Kaillera_End_Game = Empty_Kaillera_End_Game;

    Kaillera_Initialised = 0;
    return 1;
}

上面会加载这个对战库的dll,然后获取到对应的函数指针,对应的也即是对战库提供的头文件的对应的函数指针,获取到之后,存储到本地变量中,方便下次使用
DLLEXP kailleraGetVersion(char *version);
/*
    kailleraInit:
    Call this method when your program starts
*/
DLLEXP kailleraInit();
/*
    kailleraShutdown:
    Call this method when your program ends
*/
DLLEXP kailleraShutdown();
....

之后执行 DoNetGame 完成对战库的设置
static void DoNetGame()
{
    kailleraInfos ki;
    char tmpver[128];
    char* gameList;

    ....获取到当前引擎支持的游戏列表
    gameList = CreateKailleraList();

    ki.appName = tmpver;
    ki.gameList = gameList;
    //设置游戏成功的回调
    ki.gameCallback = &gameCallback;
    //设置收到聊天的回调
    ki.chatReceivedCallback = &kChatCallback;
    //设置客户端掉线的回调
    ki.clientDroppedCallback = &kDropCallback;
    ki.moreInfosCallback = NULL;

    //调用Kaillera对战库的方法,传递参数
    Kaillera_Set_Infos(&ki);
    //kailleraSetInfos(&ki);

    //显示对战库的对话框
    Kaillera_Select_Server_Dialog(NULL);
    //kailleraSelectServerDialog(NULL);
    ....
}

这样就进入到了对战库的对话框了,而且可以看到如果我们要写测试demo,我们的测试代码可以像上面这样写,我们先看看启动游戏的回调
static int WINAPI gameCallback(char* game, int player, int numplayers)
{
    bool bFound = false;
    HWND hActive;

    //根据对战库传递过来的引擎的名称,从我们的引擎列表中查找是否有对应的引擎
    for (nBurnDrvActive = 0; nBurnDrvActive < nBurnDrvCount; nBurnDrvActive++) {

        char* szDecoratedName = DecorateGameName(nBurnDrvActive);

        if (!strcmp(szDecoratedName, game)) {
            bFound = true;
            break;
        }
    }

    //如果找不到,就销毁Kailler这个对战库
    if (!bFound) {
        Kaillera_End_Game();
        return 1;
    }

    //标识当前是对战的模式
    kNetGame = 1;
    ...                                
    DrvInit(nBurnDrvActive, false);                        // Init the game driver
    ScrnInit();
    AudSoundPlay();                                        // Restart sound
    VidInit();
    SetFocus(hScrnWnd);
    ...
    RunMessageLoop();
    ...
}
上面的逻辑跟单机实现是很相似的,不同的就是,这里查找对应的引擎 对应的索引,是根据对战库传递的游戏名称来获取到的

相应的收到的聊天的回调,直接在界面上显示既可
static void WINAPI kChatCallback(char* nick, char* text)
{
    TCHAR szTemp[128];
    _sntprintf(szTemp, 128, _T("%.32hs "), nick);
    VidSAddChatMsg(szTemp, 0xBFBFFF, ANSIToTCHAR(text, NULL, 0), 0x7F7FFF);
}

这个对战库只是用来传递当前用户的按键情况,由于是实时的,所以响应的代码也是在界面的渲染地方
static int RunFrame(int bDraw, int bPause)
{
    ....
    if (kNetGame) {
        GetInput(true);                        // Update inputs   获取到当前的按键情况
        if (KailleraGetInput()) {            // Synchronize input with Kaillera   同步按键信息
            return 0;
        }
    }

    //完成界面的渲染
    BurnDrvFrame();
    ...
}

KailleraGetInput() 后面的文章继续介绍

文章作者: AheadSnail
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AheadSnail !
评论
  目录