张银峰的编程课堂

重构

在继续向下之前,我们决定对现阶段代码进行重构,这里主要有两个目标:

  • 模块不暴露内部实现,所有功能由接口实现。
  • 仅在两个模块间需要交互的类型,由模块的头文件定义。

我们遵循.h提供接口,.c负责实现细节的策略,并从模块的角度对文件进行物理划分,其中:

  • main.c: 负责构建程序主框架,与其它模块通过接口交互
  • Game.h: 提供整个游戏控制接口
  • Game.c: 提供整个游戏实现

Game.h

#pragma once

#define MAP_WIDTH   16 * 35 // 地图区域宽度
#define MAP_HEIGHT  16 * 35 // 地图区域高度

// 移动方向
typedef enum tagMOVEDIR
{
    MOVE_LEFT,
    MOVE_RIGHT,
    MOVE_UP,
    MOVE_DOWN
} MOVEDIR;

// 游戏初始化
bool Game_Init();

// 加载指定关卡的地图
bool Game_Load_Level(int level);

// 渲染游戏场景
void Game_Render();

// 玩家移动控制
void Game_Player_Move(MOVEDIR dir);

// 获取当前关卡
int Game_Get_Current_Level();

这里MAP_WIDTH, MAP_HEIGHT宏的定义可能并不直观,另一种方案是提供相应的接口函数,返回地图的宽度信息,如:

void Game_Get_MapSize(int *w, int *h);

由于MOVEDIR需要在Main.c中与键盘交互使用,因此这个枚举属于接口定义的一部分。

剩余接口函数显而易见,完全可以满足现有的设计。

Game.c

整体游戏逻辑的实现,现在由Game.c实现,这里主要有两个变化:

  • Game_Init(): 实现了资源初始化与关卡加载
  • 全局变量增加了static修饰。
#include "Easy2D.h"
#include "Game.h"

#define MAP_ROWS    16  // 地图行数
#define MAP_COLUMNS 16  // 地图列数
#define TILE_WIDTH  35  // 地面贴片元素宽度
#define TILE_HEIGHT 35  // 地面贴片元素宽度

// 以行列表示的位置信息
typedef struct tagPOS
{
    char r;
    char c;
} POS;

// 纹理信息
typedef struct tagTEXINFO
{
    int tex;
    int dx;
    int dy;
} TEXINFO;

// 场景资源
static TEXINFO g_textures[TEX_COUNT];

// 当前关卡
static int g_cur_level = 1;

// 场景地图
static char g_map[MAP_ROWS][MAP_COLUMNS];

// 玩家贴图
static int g_player_tex = E2D_INVALID_RESID;

// 玩家位置
static POS g_player_pos;

bool Game_Init()
{
    if (!Load_Resource())
        return false;

    if (!Game_Load_Level(1))
        return false;

    return true;
}

Main.c

现在,主框架程序仅是通过接口将模块驱动起来。

#include <stdio.h>
#include <stdbool.h>

#include "Easy2D.h"
#include "Game.h"

//=============================================================================
// 绘制场景
//=============================================================================
void Render()
{
    Game_Render();
}

//=============================================================================
// 键盘事件处理
//=============================================================================
void Keyboard(E2DKeyboardEvent kbe, E2DScancode sc)
{
    if (kbe == E2D_KEYDOWN)
    {
        switch (sc)
        {
        case E2D_SCANCODE_LEFT:
            Game_Player_Move(MOVE_LEFT);
            break;
        case E2D_SCANCODE_RIGHT:
            Game_Player_Move(MOVE_RIGHT);
            break;
        case E2D_SCANCODE_UP:
            Game_Player_Move(MOVE_UP);
            break;
        case E2D_SCANCODE_DOWN:
            Game_Player_Move(MOVE_DOWN);
            break;
        }
    }
}

//=============================================================================
// 鼠标事件处理
//=============================================================================
void Mouse(E2DMouseEvent me, E2DMouseButton button, int x, int y, int lbs, int mbs, int rbs)
{
}

//=============================================================================
// 程序入口点
//=============================================================================
int main()
{
    if (!E2D_Init("PushBox", MAP_WIDTH, MAP_HEIGHT))
    {
        E2D_MessageBox("PushBox", "Easy2D Initialize Failed!", E2D_STOP);
        return -1;
    }

    if (!Game_Init())
    {
        E2D_MessageBox("PushBox", "Game Initialize Failed!", E2D_STOP);
        return -2;
    }

    E2D_RenderFunc(Render);
    E2D_KeyboardFunc(Keyboard);
    E2D_MouseFunc(Mouse);
    E2D_Run();
    E2D_Release();

    return 0;
}

关闭控制台

现在我们已经掌握了事件处理,可以关闭控制台了,这是通过配置VS来实现的,有两个配置点:

  • 将【链接器】【系统】下的【子系统】选择为【窗口/SUBSYSTEM:WINDOWS】
  • 将【链接吕】【高级】下的【入口点】输入为【mainCRTStartup】

glimix.com

glimix.com