iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 16
3

引言

昨天我們總算把Renderer完成啦~

今天我們來設計按鍵對應各項功能,
其中會呼叫各個我們前幾天完成的函數。

今天大致會有的按鍵功能有:

  1. 攝影機往前、往後、往左、往右移動(w,s,a,d)
  2. 攝影機沿x軸旋轉、沿y軸旋轉(四個方向鍵)
  3. 攝影機向上平移、向下平移(b,n)
  4. 離開程式(Esc)

這些按鍵處理會寫在main的無窮迴圈中,每一幀都會讀取是否有對應按鈕按下,
若有,則進行對應動作(ex: 按下w,攝影機向前移動)


引入Renderer並初始化

/* File: main.c */

#include <stdio.h>  // 標準輸入輸出
#include <stdlib.h>  // 標準函式庫
#include <windows.h>  // 控制視窗用
#include <math.h>  // 數學函式庫(三角函數、四捨五入等等)

#include "RenderMemory.h"  // 繪製記憶體
#include "SystemSetting.h"  // 系統設定
#include "GameStatus.h"  // 遊戲狀態

// ------------新增部分-------------------
#include "Renderer.h"  // 繪製器

#define M_PI acos(-1.0)  // 因為cos(PI) = -1,所以用arccos(-1)來反向定義PI。
                         // 但此方法須引入"math.h"標頭檔
                         // p.s: PI將在旋轉角度的判讀上會用到
// --------------------------------------

int main(int argc, char *argv[])
{
    init_render_memory();  // 先初始化memory

    setting_system();  // 設置視窗屬性

    initGameStatus();  // 初始化遊戲狀態(畫面是否更新初始化為True)

// ------------新增部分-------------------
    init_renderer();  // 初始化Renderer,設置好所有相關變數值
// --------------------------------------

    return 0;
}

主要無窮迴圈—更新畫面

當偵測到上一周期繪製記憶體被改變了(isFrameUpdated被設為True),
此區將被執行,清空畫面重繪畫面

/* File: main.c */

    .
    ..
    ...

// 以下寫在init_renderer函數之下

    int i = 0, j = 0;  // 設置好給for迴圈使用的計數器

    for(;;)  /* 主要的無窮迴圈,每循環一次一個週期 */
    {
        if(isFrameUpdated)  // 若上個週期更新了Renderer中的變數,
        {                   // 則isFrameUpdated已被設為True,此週期將清空原畫面並以新變數重繪
            selfCls();  // 游標移到左上角,準備覆蓋舊畫面
            for(i=0;i<SCREEN_HEIGHT;i++)
            {
                for(j=0;j<SCREEN_WIDTH;j++)
                {
                    printf("%c", render_memory[i][j]);  // 印出繪製記憶體中的畫面
                }
            }

            // 以下部分為Debug用,可看出各項變數變化
            printf("Camera x: %f\n", camera_x_pos);
            printf("Camera y: %f\n", camera_y_pos);
            printf("Camera z: %f\n", camera_z_pos);
            printf("sin x: %f\n", sin_x);
            printf("sin y: %f\n", sin_y);
            printf("cos x: %f\n", cos_x);
            printf("cos y: %f\n", cos_y);
            printf("rotate x: %f\n", rot_x);
            printf("rotate y: %f\n", rot_y);
            printf("FOV: %f\n", fov);

            isFrameUpdated = False;  // 將上一周期被設為True的isFrameUpdated設回False
        }
    }

主要無窮迴圈—按鍵讀取(控制攝影機)

此部分為每一週期皆執行,讀取按鍵的訊息,執行不同動作。
當此週期按下按鍵,使Renderer中的變數更改後,會將isFrameUpdated設為True,
則下一週期將會執行以上更新畫面的部分。

反過來說,只要不按按鍵、不改變變數值,則畫面皆不會更新

在進入程式碼前,關於w,a,s,d的往前、往後、往左、往右,以及方向鍵的旋轉,我想先做點補充:

先講以方向鍵來控制的旋轉吧!
C語言中,"math.h"標頭檔中所定義的三角函數,參數預設是使用弧度(Radian)哦!
簡單來說,就是把0~360度換成0~https://chart.googleapis.com/chart?cht=tx&amp;chl=2%5Cpi%20 來算,轉換公式就是https://chart.googleapis.com/chart?cht=tx&amp;chl=radian%20%3D%20degree%5E%7B%5Ccirc%7D%20%5Ctimes%20%5Cfrac%7B%5Cpi%20%7D%7B180%7D 。舉個例子:https://chart.googleapis.com/chart?cht=tx&amp;chl=180%5E%7B%5Ccirc%7D 轉成弧度就會是https://chart.googleapis.com/chart?cht=tx&amp;chl=180%5E%7B%5Ccirc%7D%20%5Ctimes%20%5Cfrac%7B%5Cpi%20%7D%7B180%7D 也就是https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cpi ,而在這支程式裡我們已經定義了https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cpi ,也就是M_PI這個macro囉!這樣一來我們等一下就能用https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cpi 來描述我們要表示的角度了。

再來,我們來講講往前、往後、往左、往右,再直覺上來說,大家可能會認為這部分的處理,只要單純:

    camera_x_pos += 5;  // 例如此例子是往右5單位

之類的就行了吧?
但我想說的是,攝影機可能已經事先按了方向鍵「旋轉」過了,在我們目前的座標系中,想要往「旋轉過的座標系的右方」移動的話,必須先將該原始移動方向乘上適當的三角函數值。 感覺很抽象吧!我們來看以下的圖:
https://ithelp.ithome.com.tw/upload/images/20190930/20111429mKgnA0BWnY.png
其實如果在攝影機已旋轉https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Ctheta 度的情況下,要向右移動5的處理就會變成:

    camera_x_pos += 5 * cos(theta);
    camera_z_pos += 5 * sin(theta);

這就會做到角度旋轉後的修正了!

那麼以下就是程式碼囉!


/* File: main.c */

    for(;;)  /* 主要的無窮迴圈,每循環一次一個週期 */
    {
        if(isFrameUpdated)  // 若上個週期更新了Renderer中的變數,
        {                   // 則isFrameUpdated已被設為True,此週期將清空原畫面並以新變數重繪
            selfCls();  // 游標移到左上角,準備覆蓋舊畫面
            for(i=0;i<SCREEN_HEIGHT;i++)
            {
                for(j=0;j<SCREEN_WIDTH;j++)
                {
                    printf("%c", render_memory[i][j]);  // 印出繪製記憶體中的畫面
                }
            }

            // 以下部分為Debug用,可看出各項變數變化
            printf("Camera x: %f\n", camera_x_pos);
            printf("Camera y: %f\n", camera_y_pos);
            printf("Camera z: %f\n", camera_z_pos);
            printf("sin x: %f\n", sin_x);
            printf("sin y: %f\n", sin_y);
            printf("cos x: %f\n", cos_x);
            printf("cos y: %f\n", cos_y);
            printf("rotate x: %f\n", rot_x);
            printf("rotate y: %f\n", rot_y);
            printf("FOV: %f\n", fov);

            isFrameUpdated = False;  // 將上一周期被設為True的isFrameUpdated設回False
        }

// ------------新增部分-------------------
/* 此部分在前幾天講Windows API時有提到,GetAsyncKeyState函數能取得參數按鍵是否被按的訊號 */

        if(GetAsyncKeyState(87) != 0)  /*W*/ //前進  // W是否被按下
        {
            render_screen(_CLEAN_MODE_);  // 清空舊有畫面
            camera_z_pos += cos_y * camera_speed;  // 在有旋轉角度的情形下,
            camera_x_pos -= sin_y * camera_speed;  // 直線前進需要考慮x, z方向分量
            render_screen(_RENDER_MODE_);  // 繪製
            isFrameUpdated = True;  // 下一週期將更新到畫面上
        }
        if(GetAsyncKeyState(83) != 0)  /*S*/ //後退 // S是否被按下
        {
            render_screen(_CLEAN_MODE_);
            camera_z_pos -= cos_y * camera_speed;
            camera_x_pos += sin_y * camera_speed;
            render_screen(_RENDER_MODE_);
            isFrameUpdated = True;
        }
        if(GetAsyncKeyState(65) != 0)  /*A*/ //左移 // A是否被按下
        {
            render_screen(_CLEAN_MODE_);
            camera_z_pos -= sin_y * camera_speed;
            camera_x_pos -= cos_y * camera_speed;
            render_screen(_RENDER_MODE_);
            isFrameUpdated = True;
        }
        if(GetAsyncKeyState(68) != 0)  /*D*/ //右移 // D是否被按下
        {
            render_screen(_CLEAN_MODE_);
            camera_z_pos += sin_y * camera_speed;
            camera_x_pos += cos_y * camera_speed;
            render_screen(_RENDER_MODE_);
            isFrameUpdated = True;
        }
        if(GetAsyncKeyState(66) != 0)  /*B*/  //上升  // B是否被按下
        {
            render_screen(_CLEAN_MODE_);
            camera_y_pos -= 2;  // 旋轉並不影響上下移動,直接更改攝影機y位置
            render_screen(_RENDER_MODE_);
            isFrameUpdated = True;
        }
        if(GetAsyncKeyState(78) != 0)  /*N*/  //下降  // N是否被按下
        {
            render_screen(_CLEAN_MODE_);
            camera_y_pos += 2;
            render_screen(_RENDER_MODE_);
            isFrameUpdated = True;
        }
        if(GetAsyncKeyState(VK_RIGHT) != 0 || GetAsyncKeyState(VK_LEFT) != 0)
        {
            render_screen(_CLEAN_MODE_);
            if(GetAsyncKeyState(VK_RIGHT) != 0)  // 沿y軸旋轉,類似頭左右擺動,由左右方向鍵控制
            {
                rot_y -= camera_speed * 0.006;  // 0.006是一個調整過的參數,大家可以斟酌此數值
            }                                   // 讓旋轉幅度變大:加大此數值,反之則減小
            if(GetAsyncKeyState(VK_LEFT) != 0)
            {
                rot_y += camera_speed * 0.006;
            }
            if(rot_y > M_PI * 2 || rot_y < -(M_PI * 2)) rot_y = 0.0;  // 當旋轉到360度與
            render_screen(_RENDER_MODE_);                             // -360度時,歸零
            isFrameUpdated = True;                                    // 確保可無限旋轉
        }
        if(GetAsyncKeyState(VK_UP) != 0 || GetAsyncKeyState(VK_DOWN) != 0)
        {
            render_screen(_CLEAN_MODE_);
            if(GetAsyncKeyState(VK_UP) != 0)  // 沿x軸旋轉,類似頭上下擺動,由上下方向鍵控制
            {
                rot_x -= camera_speed * 0.006;
            }
            if(GetAsyncKeyState(VK_DOWN) != 0)
            {
                rot_x += camera_speed * 0.006;
            }
            if(rot_x > M_PI / 10) rot_x = M_PI / 10;  // 這邊我設定成上仰18度時就停止,
            else if(rot_x < -(M_PI / 10)) rot_x = -(M_PI / 10);  // 往下18度時也停止。
            render_screen(_RENDER_MODE_);
            isFrameUpdated = True;
        }
        if(GetAsyncKeyState(VK_ESCAPE) != 0)  // 按下Esc時,遊戲結束!
        {
            return 0;
        }
// --------------------------------------

    }

測試

終於終於,我們把第一版3D遊戲完成啦~~
馬上就來測試看看吧!

https://ithelp.ithome.com.tw/upload/images/20190930/20111429ZrmLzW6MUM.png
執行後我們可以看到空白一片,以及所有初始化後的Debug數值。

這時大家可以按下w,a,s,d,方向鍵,b,n等按鍵,會發現我們一開始設計的大平台出現囉!
(大家可以旋轉、平移到合適觀察的角度)
https://ithelp.ithome.com.tw/upload/images/20190930/20111429KGRDQCHDCD.png

可以試試不同角度! 再次整理一下按鍵清單:

按鍵 功能
w 攝影機前進
s 攝影機後退
a 攝影機左移
d 攝影機右移
方向鍵左右 攝影機沿y軸轉動(左右擺頭)
方向鍵上下 攝影機沿x軸轉動(上下擺頭)
b 攝影機上升
n 攝影機下降

https://ithelp.ithome.com.tw/upload/images/20190930/20111429bEICSzWHa9.png
再一張不同角度


尾聲

我們把3D引擎第一版完成啦!
明天我們要加上方塊放置功能,敬請期待~


上一篇
[11屆鐵人賽Day15] 3D引擎製作(五)—Renderer(下)
下一篇
[11屆鐵人賽Day17] 3D引擎製作(七)—放置方塊(上)(struct, linklist介紹)
系列文
若沒有遊戲引擎、合作夥伴...做得出遊戲嗎? 不試試看不知道吧? [使用C語言]30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言