www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當(dāng)前位置:首頁 > 公眾號(hào)精選 > CPP開發(fā)者


交互式編程是在程序運(yùn)行時(shí)對其進(jìn)行修改和擴(kuò)展。對于一些非批處理程序,它在開發(fā)過程中需要做大量乏味的測試和調(diào)試。直到上周,我才知道如何在 C 語言中應(yīng)用交互式編程。如何重新定義正在 C 程序中運(yùn)行的函數(shù)。

上周在 Handmade Hero(第21~25天)中,Casey Muratori 將交互式編程添加到了游戲引擎中。這在游戲開發(fā)中是特別有用的,可能游戲開發(fā)者想要在玩的過程中去調(diào)整,而不必在每次調(diào)整后重新啟動(dòng)整個(gè)游戲?,F(xiàn)在我已經(jīng)看到他明顯完成了。秘訣是將幾乎整個(gè)應(yīng)用構(gòu)建為共享庫。

這也嚴(yán)重的限制了程序的設(shè)計(jì):它不能在全局或者靜態(tài)變量中保留任何狀態(tài),盡管無論如何都應(yīng)該避免這種情況。每次重載共享庫時(shí),全局狀態(tài)都會(huì)丟失。在某些情況下,這還會(huì)限制C 標(biāo)準(zhǔn)庫的使用,包括像malloc()之類的函數(shù),但是否限制其使用具體取決于這些函數(shù)實(shí)現(xiàn)和鏈接的方式。例如,如果C 標(biāo)準(zhǔn)庫是靜態(tài)鏈接的,具有全局狀態(tài)的函數(shù)可能會(huì)將全局狀態(tài)引入到共享庫中。這很難去知道什么是安全使用的。C語言交互式編程在 Handmade Hero 中工作得還不錯(cuò)是因?yàn)閮?nèi)核游戲(作為共享庫加載的部分)不使用外部庫,包括標(biāo)準(zhǔn)庫。

此外,共享庫在使用函數(shù)指針時(shí)必須小心。在共享庫重載后,函數(shù)指針的對象將不再存在。將交互式編程與面向?qū)ο蟮?C 結(jié)合使用時(shí),這是一個(gè)實(shí)際的問題。

例子:The Game of Life

為了演示它是如何工作的,讓我們看一個(gè)例子。我編寫了一個(gè)簡單的 Game of Life 的演示,該演示很容易修改。如果你想在類似 Unix 系統(tǒng)下跑跑它,可以在這里獲取整個(gè)源代碼。

https://github.com/skeeto/interactive-c-demo

快速入門

  1. 在一個(gè)終端運(yùn)行make,然后./main,按r鍵使其不規(guī)則分布,按q鍵退出。

  2. 編輯game.c以改變 Game of Life 的規(guī)則,添加顏色等。

  3. 在另一個(gè)終端運(yùn)行make,你的改變將立刻顯示在原始程序中!

(GIF 動(dòng)圖幀數(shù)大于 300幀,超過微信平臺(tái)限制了,故而用截圖了)

在撰寫本文時(shí),Handmade Hero 是在 Windows 上編寫的,所以 Casey 使用的是 DLL 和 Win32 API。但是也可以使用libdl在 Linux 或者任何其它類 Unix 系統(tǒng)上,接下來的例子就是這么用的。

該程序分為兩部分:The Game of Life 共享庫(“game”)和封裝器(“main”),封裝器的作用是加載共享庫,當(dāng)它更新的時(shí)候重載它并在一個(gè)定期的間隔調(diào)用它。因?yàn)榉庋b器與“game”部分的操作無關(guān),所以它能在另外的項(xiàng)目中幾乎不改動(dòng)的重復(fù)使用它。

為了避免在多個(gè)位置維護(hù)一堆函數(shù)指針,將“game”的API封裝在一個(gè)結(jié)構(gòu)中。這也消除了C編譯器關(guān)于數(shù)據(jù)和函數(shù)指針混合的警告。Game_state結(jié)構(gòu)的布局和內(nèi)容對 game 本身來說是私有(private)的,封裝器僅僅處理指向該結(jié)構(gòu)的指針。

struct game_state;

struct game_api {

  struct game_state *(*init)();

  void (*finalize)(struct game_state *state);

  void (*reload)(struct game_state *state);

  void (*unload)(struct game_state *state);

  bool (*step)(struct game_state *state);

};

在該演示中,API由5個(gè)函數(shù)組成,前4個(gè)函數(shù)主要涉及裝載和卸載。

  1. Init():分配并返回要傳遞給其他每個(gè)API調(diào)用的狀態(tài)。程序啟動(dòng)時(shí)將調(diào)用一次,但重新加載后不會(huì)調(diào)用。如果我們擔(dān)心在共享庫中使用malloc(),則封裝器將負(fù)責(zé)執(zhí)行實(shí)際的內(nèi)存分配。

  2. Finalize():與init()相反,以釋放游戲狀態(tài)所擁有的所有資源。

  3. Reload():重新加載庫后立即調(diào)用。這是在運(yùn)行的程序中進(jìn)行一些其他初始化的機(jī)會(huì)。通常,此功能為空。它僅在開發(fā)期間臨時(shí)使用。

  4. Unload():在卸載庫之前,在加載新版本之前調(diào)用。這是為庫的下一版本準(zhǔn)備的機(jī)會(huì)。如果您要非常小心的話,可以使用它來更新結(jié)構(gòu)等。通常也為空。

  5. Step():定期調(diào)用以運(yùn)行游戲。一個(gè)真正的游戲可能會(huì)具有更多這樣的功能。

該庫將提供一個(gè)填充的 API 結(jié)構(gòu)作為全局變量 GAME_API。**這是整個(gè)共享庫中唯一導(dǎo)出的符號(hào)!**所有函數(shù)都將聲明為靜態(tài),包括該結(jié)構(gòu)所引用的函數(shù)。

const struct game_api GAME_API = {

  .init   = game_init,

  .finalize = game_finalize,

  .reload  = game_reload,

  .unload  = game_unload,

  .step   = game_step

};

dlopen,dlsym和dlclose

該封裝器的重點(diǎn)是用正確的順序,在正確的時(shí)間調(diào)用dlopen(),dlsym(),dlclose()。該游戲被編譯為libganme.so,這也就是被加載的東西。它在源代碼中用./以強(qiáng)制將名稱用作文件名。封裝器追溯game結(jié)構(gòu)中的所有內(nèi)容。

const char *GAME_LIBRARY = "./libgame.so";

struct game {

  void *handle;

  ino_t id;

  struct game_api api;

  struct game_state *state;

};

該handle是dlopen()的返回值,id是共享庫的索引節(jié)點(diǎn),是stat()的返回值。其余的定義如上所示。為什么是索引節(jié)點(diǎn)?我們可以改用時(shí)間戳,但它是間接的。我們真正關(guān)心的是,共享對象文件實(shí)際上是否不同于已加載的文件。該文件永遠(yuǎn)不會(huì)在合適的位置進(jìn)行更新,而是被編譯器/連接器替換,所以時(shí)間戳并不重要。

使用索引節(jié)點(diǎn)比Handmade Hero簡單的多。由于Windows的損壞文件鎖定機(jī)制,游戲DLL在使用時(shí)不能被替換。要解決此限制,構(gòu)建系統(tǒng)和加載器不得不依賴于隨機(jī)生成的文件名。

void game_load(struct game *game)

該game_load()功能的目的是將游戲API加載到game結(jié)構(gòu)中,但前提是尚未加載游戲API或已對其進(jìn)行更新。由于它具有多個(gè)獨(dú)立的故障條件,因此我們將對其進(jìn)行部分檢查。

struct stat attr; if ((stat(GAME_LIBRARY, &attr) == 0) && (game->id != attr.st_ino)) {

首先,使用stat()來確定庫的索引節(jié)點(diǎn)是否不同于已加載的索引節(jié)點(diǎn)。該id字段最初將為0,因此只要stat()返回成功,它將首次加載該庫。

if (game->handle) {

    game->api.unload(game->state);

    dlclose(game->handle);

  }

如果已經(jīng)加載了庫,請先將其卸載,請確保調(diào)用 unload()以通知庫正在更新。**確保Dlclose()在dlopen()之前調(diào)用是至關(guān)重要的。**在我的系統(tǒng)上,dlopen()僅查看給定的字符串,而不查看其背后的文件。即使文件已在文件系統(tǒng)上被替換,dlopen()也會(huì)看到該字符串與已打開的庫匹配,并返回指向舊庫的指針。(這是一個(gè)錯(cuò)誤嗎?)句柄由libdl在內(nèi)部進(jìn)行引用計(jì)數(shù)。

void *handle = dlopen(GAME_LIBRARY, RTLD_NOW);

最后加載游戲庫。由于dlopen()的限制,這里存在一個(gè)競態(tài)條件。在調(diào)用stat()之后,庫可能已經(jīng)再次更新。由于我們無法詢問dlopen()打開的庫的索引節(jié)點(diǎn),因此我們無法得知。但是由于這只是在開發(fā)過程中使用,而不是在生產(chǎn)中使用,所以這沒什么大不了的。

if (handle) {

    game->handle = handle;

    game->id = attr.st_ino;

    */\* ... more below ... \*/*

  } else {

    game->handle = NULL;

    game->id = 0;

  }

如果dlopen()失敗,它將返回NULL。在ELF的情況下,如果編譯器/鏈接器仍在寫出到共享庫的過程中,則會(huì)發(fā)生這種情況。由于卸載已經(jīng)完成,這意味著game_load返回時(shí)不會(huì)加載任何游戲。該結(jié)構(gòu)的用戶需要為此做好準(zhǔn)備,它將需要稍后(即幾毫秒)再嘗試加載。當(dāng)未加載任何庫時(shí),可以使用存根函數(shù)填充API。

const struct game_api *api = dlsym(game->handle, "GAME_API"); if (api != NULL) {

    game->api = *api; if (game->state == NULL)

      game->state = game->api.init();

    game->api.reload(game->state);

  } else {

    dlclose(game->handle);

    game->handle = NULL;

    game->id = 0;

  }

當(dāng)庫無錯(cuò)誤加載時(shí),查找前面提到的GAME_API結(jié)構(gòu)并將其復(fù)制到本地結(jié)構(gòu)中。在進(jìn)行函數(shù)調(diào)用時(shí),進(jìn)行復(fù)制而不是使用指針避免了一層重定向。如果尚未初始化游戲狀態(tài),則調(diào)用reload()函數(shù)以通知游戲它剛剛被重新加載。

如果查找GAME_API失敗,請關(guān)閉句柄并將其視為失敗。

主循環(huán)每次都調(diào)用game_load()。它就是這樣!

int main(void)
{
    struct game game = {0}; for (;;) {
        game_load(&game); if (game.handle) if (!game.api.step(game.state)) break;
        usleep(100000);
    }
    game_unload(&game); return 0;
}

現(xiàn)在,我已經(jīng)掌握了這項(xiàng)技術(shù),很想用C語言和OpenGL去開發(fā)一個(gè)完整的游戲,或許是另一個(gè)極限游戲開發(fā)。交互式開發(fā)的能力真的很令人著迷。


本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時(shí)聯(lián)系本站刪除。
關(guān)閉