Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the antispam-bee domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/ruwritingagame/public_html/wp-includes/functions.php on line 6121
Глава 24. Синхронизация – Игра в Написание Игры

Глава 24. Синхронизация

Сейчас на обоих моих платформах (Android и PC) кубик делает 1 оборот примерно за 4 секунды. Это 360 кадров (скорость вращения установлена на 1 градус за кадр). Это значит 360/4=90 кадров в секунду (frames per second, FPS). Понятно, что на других устройствах эта цифра может быть другой. Кроме того, на скорость могут влиять фоновые процессы и другие факторы. В основном, сколько и чего мы рендрим, насколько плотно занят наш экран.

Синхронизация нужна чтобы держать FPS скорость постоянной и предсказуемой. Выше FPS – мягче анимация. Ниже FPS – больше времени на более сложный рендринг.

“Золотая середина” – 30 FPS. На всякий случай, 24 (как вполне комфортное) было кино-стандартом целый век.

1000/30 дает нам 33 миллисекунды на рендринг кадра. Не так чтоб много (для сложного кадра), но мы поствраемся вписаться.

Имплементация:

1. Запускаем VS, открываем C:\CPP\a997modeler\p_windows\p_windows.sln.


В TheGame.cpp, когда очередной кадр готов, перед тем как выдать его на экран, мы будем проверять системное время и ждать пока не пройдет 33 миллисекунды от предыдущего кадра.

Для этого нам понадобится

системное время в миллисекундах.

MS последовательно совершенствует свою способность делать простые вещи сложными. Библиотека chrono – очередное достижение, поэтому современное решение будет выглядеть так:

    auto currentTime = std::chrono::system_clock::now().time_since_epoch();
    return std::chrono::duration_cast<std::chrono::milliseconds>(currentTime).count();


На стороне TheGame потребуется несколько новых переменных.

2. Заменим TheGame.h код на:

#pragma once
#include <vector>
#include "GameSubj.h"
#include "Camera.h"

class TheGame
{
public:
	int screenSize[2];
	float screenAspectRatio = 1;
	//synchronization
	long long int lastFrameMillis = 0;
	int targetFPS = 30;
	int millisPerFrame = 1000 / targetFPS;

	bool bExitGame;
	Camera mainCamera;
	float dirToMainLight[4] = { 1,1,1,0 };

	//static arrays (vectors) of active GameSubjs
	static std::vector<GameSubj*> gameSubjs;
public:
	int run();
	int getReady();
	int drawFrame();
	int cleanUp();
	int onScreenResize(int width, int height);
};


3. Заменим TheGame.cpp код на:

#include "TheGame.h"
#include "platform.h"
#include "utils.h"
#include "linmath.h"
#include "Texture.h"
#include "Shader.h"
#include "DrawJob.h"
#include "ModelBuilder.h"
#include "TexCoords.h"

extern std::string filesRoot;
extern float degrees2radians;

std::vector<GameSubj*> TheGame::gameSubjs;

int TheGame::getReady() {
    bExitGame = false;
    Shader::loadShaders();
    glEnable(GL_CULL_FACE);

    //=== create box ========================
    GameSubj* pGS = new GameSubj();
    gameSubjs.push_back(pGS);

    pGS->name.assign("box1");
    pGS->ownCoords.setPosition(0, 0, 0);
    pGS->ownCoords.setDegrees(0, 0, 0);
    pGS->ownSpeed.setDegrees(0,3,0);

    ModelBuilder* pMB = new ModelBuilder();
    pMB->useSubjN(gameSubjs.size() - 1);

    //define VirtualShape
    VirtualShape vs;
    vs.setShapeType("box-tank");
    vs.whl[0] = 60;
    vs.whl[1] = 160;
    vs.whl[2] = 390;
    vs.setExt(20);
    vs.extD = 0;
    vs.extF = 0; //to make front face "flat"
    vs.sectionsR = 2;

    Material mt;
    //define material - flat red
    mt.shaderN = Shader::spN_phong_ucolor;
    mt.primitiveType = GL_TRIANGLES;
    mt.uColor.setRGBA(255, 0, 0,255); //red
    pMB->useMaterial(&mt);

    pMB->buildBoxFace(pMB,"front v", &vs);
    pMB->buildBoxFace(pMB, "back v", &vs);
    pMB->buildBoxFace(pMB, "top", &vs);
    pMB->buildBoxFace(pMB, "bottom", &vs);
    pMB->buildBoxFace(pMB, "left all", &vs);

    mt.uColor.clear(); // set to zero;
    mt.uTex0 = Texture::loadTexture(filesRoot + "/dt/sample_img.png");
    mt.shaderN = Shader::spN_phong_tex;
    pMB->useMaterial(&mt);
    TexCoords tc;
    tc.set(mt.uTex0, 11, 12, 256, 128, "h"); //flip horizontally
    pMB->buildBoxFace(pMB, "right all", &vs, &tc);

    pMB->buildDrawJobs(gameSubjs);

    delete pMB;

    //===== set up camera
    mainCamera.ownCoords.setDegrees(15, 180, 0); //set camera angles/orientation
    mainCamera.viewRangeDg = 30;
    mainCamera.stageSize[0] = 500;
    mainCamera.stageSize[1] = 375;
    memcpy(mainCamera.lookAtPoint, pGS->ownCoords.pos, sizeof(float) * 3);
    mainCamera.onScreenResize();

    //===== set up light
    v3set(dirToMainLight, -1, 1, 1);
    vec3_norm(dirToMainLight, dirToMainLight);

    return 1;
}
int TheGame::drawFrame() {
    myPollEvents();

    //glClearColor(0.0, 0.0, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);

    //calculate halfVector
    float dirToCamera[4] = { 0,0,-1,0 }; //-z
    mat4x4_mul_vec4plus(dirToCamera, *mainCamera.ownCoords.getRotationMatrix(), dirToCamera, 0);

    float uHalfVector[4] = { 0,0,0,0 };
    for (int i = 0; i < 3; i++)
        uHalfVector[i] = (dirToCamera[i] + dirToMainLight[i]) / 2;
    vec3_norm(uHalfVector, uHalfVector);

    mat4x4 mProjection, mViewProjection, mMVP, mMV4x4;
    //mat4x4_ortho(mProjection, -(float)screenSize[0] / 2, (float)screenSize[0] / 2, -(float)screenSize[1] / 2, (float)screenSize[1] / 2, 100.f, 500.f);
    float nearClip = mainCamera.focusDistance - 250;
    float farClip = mainCamera.focusDistance + 250;
    mat4x4_perspective(mProjection, mainCamera.viewRangeDg * degrees2radians, screenAspectRatio, nearClip, farClip);
    mat4x4_mul(mViewProjection, mProjection, mainCamera.lookAtMatrix);
    //mViewProjection[1][3] = 0; //keystone effect

    //scan subjects
    int subjsN = gameSubjs.size();
    for (int subjN = 0; subjN < subjsN; subjN++) {
        GameSubj* pGS = gameSubjs.at(subjN);
        //behavior - apply rotation speed
        pGS->moveSubj();
        //prepare subject for rendering
        pGS->buildModelMatrix(pGS);
        //build MVP matrix for given subject
        mat4x4_mul(mMVP, mViewProjection, pGS->ownModelMatrix);
        //build Model-View (rotation) matrix for normals
        mat4x4_mul(mMV4x4, mainCamera.lookAtMatrix, (vec4*)pGS->ownCoords.getRotationMatrix());
        //convert to 3x3 matrix
        float mMV3x3[3][3];
        for (int y = 0; y < 3; y++)
            for (int x = 0; x < 3; x++)
                mMV3x3[y][x] = mMV4x4[y][x];
        //render subject
        for (int i = 0; i < pGS->djTotalN; i++) {
            DrawJob* pDJ = DrawJob::drawJobs.at(pGS->djStartN + i);
            pDJ->execute((float*)mMVP, *mMV3x3, dirToMainLight, uHalfVector, NULL);
        }
    }
    //synchronization
    while (1) {
        long long int currentMillis = getSystemMillis();
        long long int millisSinceLastFrame = currentMillis - lastFrameMillis;
        if (millisSinceLastFrame >= millisPerFrame) {
            lastFrameMillis = currentMillis;
            break;
        }
    }
    mySwapBuffers();
    return 1;
}

int TheGame::cleanUp() {
    int itemsN = gameSubjs.size();
    //delete all UISubjs
    for (int i = 0; i < itemsN; i++) {
        GameSubj* pGS = gameSubjs.at(i);
        delete pGS;
    }
    gameSubjs.clear();
    //clear all other classes
    Texture::cleanUp();
    Shader::cleanUp();
    DrawJob::cleanUp();
    return 1;
}
int TheGame::onScreenResize(int width, int height) {
    if (screenSize[0] == width && screenSize[1] == height)
        return 0;
    screenSize[0] = width;
    screenSize[1] = height;
    screenAspectRatio = (float)width / height;
    glViewport(0, 0, width, height);
    mainCamera.onScreenResize();
    mylog(" screen size %d x %d\n", width, height);
    return 1;
}
int TheGame::run() {
    getReady();
    while (!bExitGame) {
        drawFrame();
    }
    cleanUp();
    return 1;
}

  • Поскольку FPS теперь в 3 раза ниже (30 вместо 90 ранее), можно увеличить скорость вращения кубика (строка 28).

Функцию getSystemMillis() мы расположим в наборе utils.

4. Заменим utils.h код на:

#pragma once
#include <string>
#include <vector>
#include "linmath.h"

int checkGLerrors(std::string ref);
void mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3);

void v3set(float* vOut, float x, float y, float z);
void v3copy(float* vOut, float* vIn);
float v3pitchRd(float* vIn);
float v3yawRd(float* vIn);
float v3pitchDg(float* vIn);
float v3yawDg(float* vIn);

long long int getSystemMillis();
long long int getSystemNanos();

int getRandom(int fromN, int toN);
float getRandom(float fromN, float toN);
std::vector<std::string> splitString(std::string inString, std::string delimiter);
std::string trimString(std::string inString);
bool fileExists(const char* filePath);
std::string getFullPath(std::string filePath);
std::string getInAppPath(std::string filePath);
int makeDirs(std::string filePath);

  • Пользуясь поводом, я добавип еще несколько полезных функций, так что getSystemMillis() – не единственное изменение.

5. Заменим utils.cpp код на:

#include "utils.h"
#include "platform.h"
#include <chrono>
#include <stdlib.h>     /* srand, rand */
#include <sys/stat.h> //if fileExists
#include <time.h> //for srand()

extern std::string filesRoot;
extern float radians2degrees;

int checkGLerrors(std::string ref) {
    //can be used after any GL call
    int res = glGetError();
    if (res == 0)
        return 0;
    std::string errCode;
    switch (res) {
        //case GL_NO_ERROR: errCode = "GL_NO_ERROR"; break;
        case GL_INVALID_ENUM: errCode = "GL_INVALID_ENUM"; break;
        case GL_INVALID_VALUE: errCode = "GL_INVALID_VALUE"; break;
        case GL_INVALID_OPERATION: errCode = "GL_INVALID_OPERATION"; break;
        case GL_INVALID_FRAMEBUFFER_OPERATION: errCode = "GL_INVALID_FRAMEBUFFER_OPERATION"; break;
        case GL_OUT_OF_MEMORY: errCode = "GL_OUT_OF_MEMORY"; break;
        default: errCode = "??"; break;
    }
    mylog("GL ERROR %d-%s in %s\n", res, errCode.c_str(), ref.c_str());
    return -1;
}
void mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3) {
    vec4 v2;
    if (vOut == vIn) {
        memcpy(&v2, vIn, sizeof(vec4));
        vIn = v2;
    }
    vIn[3] = (float)v3;
    mat4x4_mul_vec4(vOut, M, vIn);
}
void v3set(float* vOut, float x, float y, float z) {
    vOut[0] = x;
    vOut[1] = y;
    vOut[2] = z;
}
void v3copy(float* vOut, float* vIn) {
    for (int i = 0; i < 3; i++)
        vOut[i] = vIn[i];
}
float v3yawRd(float* vIn) {
    return atan2f(vIn[0], vIn[2]);
}
float v3pitchRd(float* vIn) {
    return -atan2f(vIn[1], sqrtf(vIn[0] * vIn[0] + vIn[2] * vIn[2]));
}
float v3pitchDg(float* vIn) { 
    return v3pitchRd(vIn) * radians2degrees; 
}
float v3yawDg(float* vIn) { 
    return v3yawRd(vIn) * radians2degrees; 
}

long long int getSystemMillis() {
    auto currentTime = std::chrono::system_clock::now().time_since_epoch();
    return std::chrono::duration_cast<std::chrono::milliseconds>(currentTime).count();
}
long long int getSystemNanos() {
    auto currentTime = std::chrono::system_clock::now().time_since_epoch();
    return std::chrono::duration_cast<std::chrono::nanoseconds>(currentTime).count();
}
int randomCallN = 0;
int getRandom() {
    if (randomCallN % 1000 == 0)
        srand((unsigned int)getSystemNanos()); //re-initialize random seed:
    randomCallN++;
    return rand();
}
int getRandom(int fromN, int toN) {
    int randomN = getRandom();
    int range = toN - fromN + 1;
    return (fromN + randomN % range);
}
float getRandom(float fromN, float toN) {
    int randomN = getRandom();
    float range = toN - fromN;
    return (fromN + (float)randomN / RAND_MAX * range);
}
std::vector<std::string> splitString(std::string inString, std::string delimiter) {
    std::vector<std::string> outStrings;
    int delimiterSize = delimiter.size();
    outStrings.clear();
    while (inString.size() > 0) {
        int delimiterPosition = inString.find(delimiter);
        if (delimiterPosition == 0) {
            inString = inString.substr(delimiterSize, inString.size() - delimiterSize);
            continue;
        }
        if (delimiterPosition == std::string::npos) {
            //last element
            outStrings.push_back(trimString(inString));
            break;
        }
        std::string outString = inString.substr(0, delimiterPosition);
        outStrings.push_back(trimString(outString));
        int startAt = delimiterPosition + delimiterSize;
        inString = inString.substr(startAt, inString.size() - startAt);
    }
    return outStrings;
}
std::string trimString(std::string inString) {
    //Remove leading and trailing spaces
    int startsAt = inString.find_first_not_of(" ");
    if (startsAt == std::string::npos)
        return "";
    int endsAt = inString.find_last_not_of(" ") + 1;
    return inString.substr(startsAt, endsAt - startsAt);
}
bool fileExists(const char* filePath) {
    struct stat info;
    if (stat(filePath, &info) == 0)
        return true;
    else
        return false;
}
std::string getFullPath(std::string filePath) {
    if (filePath.find(filesRoot) == 0)
        return filePath;
    else
        return (filesRoot + filePath);
}
std::string getInAppPath(std::string filePath) {
    std::string inAppPath(filePath);
    if (inAppPath.find(filesRoot) == 0) {
        int startsAt = filesRoot.size();
        inAppPath = inAppPath.substr(startsAt, inAppPath.size() - startsAt);
    }
    if (inAppPath.find(".") != std::string::npos) {
        //cut off file name
        int endsAt = inAppPath.find_last_of("/");
        inAppPath = inAppPath.substr(0, endsAt + 1);
    }
    return inAppPath;
}
int makeDirs(std::string filePath) {
    filePath = getFullPath(filePath);
    std::string inAppPath = getInAppPath(filePath);
    std::vector<std::string> path = splitString(inAppPath, "/");
    int pathSize = path.size();
    filePath.assign(filesRoot);
    for (int i = 0; i < pathSize; i++) {
        filePath.append("/" + path.at(i));
        if (fileExists(filePath.c_str())) {
            continue;
        }
        //create dir
        myMkDir(filePath.c_str());
        mylog("Folder %d: %s created.\n", i, filePath.c_str());
    }
    return 1;
}

Еще добавлены:

  • getRandom() – скоро нам понядобятся случайные числа. В C++ есть функция rand(), которая генерирует псевдо-случайные integer в диапазоне от 0 до RAND_MAX. Для удобства мы завернем ее в пару других функций, возвращающих случайный int или float в заданном диапазоне.
  • Также добавлены getFullPath() и getInAppPath() из FileLoader.
  • Еще пара функций для работы со строками: splitString(…) – разбить строку и trimString(…)– убрать лишние пробелы.
  • И пара – для работы с файлами: проверка на наличие файла fileExists(…) и создание каталогов makeDirs(…).

6. Компиляция и запуск.

Скорость вращения выглядит так же. Значит, поставленная цель достигнута.

На Андроиде тоже проверено, анимация такая же гладкая, значит 30 FPS – правильный выбор.


Leave a Reply

Your email address will not be published. Required fields are marked *