{"id":744,"date":"2021-12-08T22:45:37","date_gmt":"2021-12-08T22:45:37","guid":{"rendered":"https:\/\/writingagame.com\/?p=744"},"modified":"2022-02-21T22:35:52","modified_gmt":"2022-02-21T22:35:52","slug":"chapter-24-synchronization","status":"publish","type":"post","link":"https:\/\/writingagame.com\/index.php\/2021\/12\/08\/chapter-24-synchronization\/","title":{"rendered":"Chapter 24. Synchronization"},"content":{"rendered":"\n<p>Right now on both my devices (Android and PC) the box makes 1 revolution in approximately 4 seconds. It&#8217;s 360 frames (rotation speed is set to 1 degree per frame). This means 360\/4=90 frames per second (FPS). Obviously, on another devices it can be different. Besides, speed can depend on background processes and other factors. Mostly, on how many objects we are rendering, how busy our screen is.<\/p>\n\n\n\n<p>Synchronization is intended to ensure consistent and predictable FPS speed. Higher FPS &#8211; smoother animation. Lower FPS &#8211; more detailed image with more objects we can render.<\/p>\n\n\n\n<p>The &#8220;golden mean&#8221; is 30 FPS. Just in case, 24 (as comfortable enough) was a movies standard for a century.<\/p>\n\n\n\n<p>1000\/30 leaves us 33 milliseconds to render a frame. Not much (if it&#8217;s complicated enough), but we will try to fit.<\/p>\n\n\n\n<p><strong>Implementation<\/strong>:<\/p>\n\n\n\n<p>1. Start VS, open <em>C:\\CPP\\<em>a997modeler<\/em>\\p_windows\\p_windows.sln<\/em>.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>In <em>TheGame.cpp<\/em>, when next frame is ready, before pushing it to the screen, we&#8217;ll check system time and will wait until we have 33 millis passed since the last frame.<\/p>\n\n\n\n<p>For this we&#8221;ll need to get a<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"system-time-in-milliseconds\">system time in milliseconds.<\/h2>\n\n\n\n<p>MS continuously keeps mastering their ability to make simple things complicated.  <em>chrono  <\/em>library &#8211; is another achievement, so modern solution will be:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; first-line: 61; title: ; notranslate\" title=\"\">\n    auto currentTime = std::chrono::system_clock::now().time_since_epoch();\n    return std::chrono::duration_cast&lt;std::chrono::milliseconds&gt;(currentTime).count();\n<\/pre><\/div>\n\n\n<p><br>On  <em>TheGame<\/em>  side it will require a few new variables.<\/p>\n\n\n\n<p>2.  Open <em>TheGame.h<\/em> and replace code by: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [11,12,13,14]; title: ; notranslate\" title=\"\">\n#pragma once\n#include &lt;vector&gt;\n#include &quot;GameSubj.h&quot;\n#include &quot;Camera.h&quot;\n\nclass TheGame\n{\npublic:\n\tint screenSize&#91;2];\n\tfloat screenAspectRatio = 1;\n\t\/\/synchronization\n\tlong long int lastFrameMillis = 0;\n\tint targetFPS = 30;\n\tint millisPerFrame = 1000 \/ targetFPS;\n\n\tbool bExitGame;\n\tCamera mainCamera;\n\tfloat dirToMainLight&#91;4] = { 1,1,1,0 };\n\n\t\/\/static arrays (vectors) of active GameSubjs\n\tstatic std::vector&lt;GameSubj*&gt; gameSubjs;\npublic:\n\tint run();\n\tint getReady();\n\tint drawFrame();\n\tint cleanUp();\n\tint onScreenResize(int width, int height);\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>3. Open <em>TheGame.cpp<\/em> and replace code by:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [28,129,130,131,132,133,134,135,136,137]; title: ; notranslate\" title=\"\">\n#include &quot;TheGame.h&quot;\n#include &quot;platform.h&quot;\n#include &quot;utils.h&quot;\n#include &quot;linmath.h&quot;\n#include &quot;Texture.h&quot;\n#include &quot;Shader.h&quot;\n#include &quot;DrawJob.h&quot;\n#include &quot;ModelBuilder.h&quot;\n#include &quot;TexCoords.h&quot;\n\nextern std::string filesRoot;\nextern float degrees2radians;\n\nstd::vector&lt;GameSubj*&gt; TheGame::gameSubjs;\n\nint TheGame::getReady() {\n    bExitGame = false;\n    Shader::loadShaders();\n    glEnable(GL_CULL_FACE);\n\n    \/\/=== create box ========================\n    GameSubj* pGS = new GameSubj();\n    gameSubjs.push_back(pGS);\n\n    pGS-&gt;name.assign(&quot;box1&quot;);\n    pGS-&gt;ownCoords.setPosition(0, 0, 0);\n    pGS-&gt;ownCoords.setDegrees(0, 0, 0);\n    pGS-&gt;ownSpeed.setDegrees(0,3,0);\n\n    ModelBuilder* pMB = new ModelBuilder();\n    pMB-&gt;useSubjN(gameSubjs.size() - 1);\n\n    \/\/define VirtualShape\n    VirtualShape vs;\n    vs.setShapeType(&quot;box-tank&quot;);\n    vs.whl&#91;0] = 60;\n    vs.whl&#91;1] = 160;\n    vs.whl&#91;2] = 390;\n    vs.setExt(20);\n    vs.extD = 0;\n    vs.extF = 0; \/\/to make front face &quot;flat&quot;\n    vs.sectionsR = 2;\n\n    Material mt;\n    \/\/define material - flat red\n    mt.shaderN = Shader::spN_phong_ucolor;\n    mt.primitiveType = GL_TRIANGLES;\n    mt.uColor.setRGBA(255, 0, 0,255); \/\/red\n    pMB-&gt;useMaterial(&amp;mt);\n\n    pMB-&gt;buildBoxFace(pMB,&quot;front v&quot;, &amp;vs);\n    pMB-&gt;buildBoxFace(pMB, &quot;back v&quot;, &amp;vs);\n    pMB-&gt;buildBoxFace(pMB, &quot;top&quot;, &amp;vs);\n    pMB-&gt;buildBoxFace(pMB, &quot;bottom&quot;, &amp;vs);\n    pMB-&gt;buildBoxFace(pMB, &quot;left all&quot;, &amp;vs);\n\n    mt.uColor.clear(); \/\/ set to zero;\n    mt.uTex0 = Texture::loadTexture(filesRoot + &quot;\/dt\/sample_img.png&quot;);\n    mt.shaderN = Shader::spN_phong_tex;\n    pMB-&gt;useMaterial(&amp;mt);\n    TexCoords tc;\n    tc.set(mt.uTex0, 11, 12, 256, 128, &quot;h&quot;); \/\/flip horizontally\n    pMB-&gt;buildBoxFace(pMB, &quot;right all&quot;, &amp;vs, &amp;tc);\n\n    pMB-&gt;buildDrawJobs(gameSubjs);\n\n    delete pMB;\n\n    \/\/===== set up camera\n    mainCamera.ownCoords.setDegrees(15, 180, 0); \/\/set camera angles\/orientation\n    mainCamera.viewRangeDg = 30;\n    mainCamera.stageSize&#91;0] = 500;\n    mainCamera.stageSize&#91;1] = 375;\n    memcpy(mainCamera.lookAtPoint, pGS-&gt;ownCoords.pos, sizeof(float) * 3);\n    mainCamera.onScreenResize();\n\n    \/\/===== set up light\n    v3set(dirToMainLight, -1, 1, 1);\n    vec3_norm(dirToMainLight, dirToMainLight);\n\n    return 1;\n}\nint TheGame::drawFrame() {\n    myPollEvents();\n\n    \/\/glClearColor(0.0, 0.0, 0.5, 1.0);\n    glClear(GL_COLOR_BUFFER_BIT);\n\n    \/\/calculate halfVector\n    float dirToCamera&#91;4] = { 0,0,-1,0 }; \/\/-z\n    mat4x4_mul_vec4plus(dirToCamera, *mainCamera.ownCoords.getRotationMatrix(), dirToCamera, 0);\n\n    float uHalfVector&#91;4] = { 0,0,0,0 };\n    for (int i = 0; i &lt; 3; i++)\n        uHalfVector&#91;i] = (dirToCamera&#91;i] + dirToMainLight&#91;i]) \/ 2;\n    vec3_norm(uHalfVector, uHalfVector);\n\n    mat4x4 mProjection, mViewProjection, mMVP, mMV4x4;\n    \/\/mat4x4_ortho(mProjection, -(float)screenSize&#91;0] \/ 2, (float)screenSize&#91;0] \/ 2, -(float)screenSize&#91;1] \/ 2, (float)screenSize&#91;1] \/ 2, 100.f, 500.f);\n    float nearClip = mainCamera.focusDistance - 250;\n    float farClip = mainCamera.focusDistance + 250;\n    mat4x4_perspective(mProjection, mainCamera.viewRangeDg * degrees2radians, screenAspectRatio, nearClip, farClip);\n    mat4x4_mul(mViewProjection, mProjection, mainCamera.lookAtMatrix);\n    \/\/mViewProjection&#91;1]&#91;3] = 0; \/\/keystone effect\n\n    \/\/scan subjects\n    int subjsN = gameSubjs.size();\n    for (int subjN = 0; subjN &lt; subjsN; subjN++) {\n        GameSubj* pGS = gameSubjs.at(subjN);\n        \/\/behavior - apply rotation speed\n        pGS-&gt;moveSubj();\n        \/\/prepare subject for rendering\n        pGS-&gt;buildModelMatrix(pGS);\n        \/\/build MVP matrix for given subject\n        mat4x4_mul(mMVP, mViewProjection, pGS-&gt;ownModelMatrix);\n        \/\/build Model-View (rotation) matrix for normals\n        mat4x4_mul(mMV4x4, mainCamera.lookAtMatrix, (vec4*)pGS-&gt;ownCoords.getRotationMatrix());\n        \/\/convert to 3x3 matrix\n        float mMV3x3&#91;3]&#91;3];\n        for (int y = 0; y &lt; 3; y++)\n            for (int x = 0; x &lt; 3; x++)\n                mMV3x3&#91;y]&#91;x] = mMV4x4&#91;y]&#91;x];\n        \/\/render subject\n        for (int i = 0; i &lt; pGS-&gt;djTotalN; i++) {\n            DrawJob* pDJ = DrawJob::drawJobs.at(pGS-&gt;djStartN + i);\n            pDJ-&gt;execute((float*)mMVP, *mMV3x3, dirToMainLight, uHalfVector, NULL);\n        }\n    }\n    \/\/synchronization\n    while (1) {\n        long long int currentMillis = getSystemMillis();\n        long long int millisSinceLastFrame = currentMillis - lastFrameMillis;\n        if (millisSinceLastFrame &gt;= millisPerFrame) {\n            lastFrameMillis = currentMillis;\n            break;\n        }\n    }\n    mySwapBuffers();\n    return 1;\n}\n\nint TheGame::cleanUp() {\n    int itemsN = gameSubjs.size();\n    \/\/delete all UISubjs\n    for (int i = 0; i &lt; itemsN; i++) {\n        GameSubj* pGS = gameSubjs.at(i);\n        delete pGS;\n    }\n    gameSubjs.clear();\n    \/\/clear all other classes\n    Texture::cleanUp();\n    Shader::cleanUp();\n    DrawJob::cleanUp();\n    return 1;\n}\nint TheGame::onScreenResize(int width, int height) {\n    if (screenSize&#91;0] == width &amp;&amp; screenSize&#91;1] == height)\n        return 0;\n    screenSize&#91;0] = width;\n    screenSize&#91;1] = height;\n    screenAspectRatio = (float)width \/ height;\n    glViewport(0, 0, width, height);\n    mainCamera.onScreenResize();\n    mylog(&quot; screen size %d x %d\\n&quot;, width, height);\n    return 1;\n}\nint TheGame::run() {\n    getReady();\n    while (!bExitGame) {\n        drawFrame();\n    }\n    cleanUp();\n    return 1;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Since now it will be 3 times slower (30 vs 90 FPS before), we can increase box rotation speed (line 28).<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>We&#8217;ll place <em>getSystemMillis()<\/em> function in <strong>utils <\/strong>set.<\/p>\n\n\n\n<p>4.  Open <em>utils.h<\/em> and replace code by: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [16]; title: ; notranslate\" title=\"\">\n#pragma once\n#include &lt;string&gt;\n#include &lt;vector&gt;\n#include &quot;linmath.h&quot;\n\nint checkGLerrors(std::string ref);\nvoid mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3);\n\nvoid v3set(float* vOut, float x, float y, float z);\nvoid v3copy(float* vOut, float* vIn);\nfloat v3pitchRd(float* vIn);\nfloat v3yawRd(float* vIn);\nfloat v3pitchDg(float* vIn);\nfloat v3yawDg(float* vIn);\n\nlong long int getSystemMillis();\nlong long int getSystemNanos();\n\nint getRandom(int fromN, int toN);\nfloat getRandom(float fromN, float toN);\nstd::vector&lt;std::string&gt; splitString(std::string inString, std::string delimiter);\nstd::string trimString(std::string inString);\nbool fileExists(const char* filePath);\nstd::string getFullPath(std::string filePath);\nstd::string getInAppPath(std::string filePath);\nint makeDirs(std::string filePath);\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>It was a nice occasion to add a few more useful functions, so <em>getSystemMillis()<\/em> is not the only update here.<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p> 5.  Open <em>utils.cpp<\/em> and replace code by:  <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [60]; title: ; notranslate\" title=\"\">\n#include &quot;utils.h&quot;\n#include &quot;platform.h&quot;\n#include &lt;chrono&gt;\n#include &lt;stdlib.h&gt;     \/* srand, rand *\/\n#include &lt;sys\/stat.h&gt; \/\/if fileExists\n#include &lt;time.h&gt; \/\/for srand()\n\nextern std::string filesRoot;\nextern float radians2degrees;\n\nint checkGLerrors(std::string ref) {\n    \/\/can be used after any GL call\n    int res = glGetError();\n    if (res == 0)\n        return 0;\n    std::string errCode;\n    switch (res) {\n        \/\/case GL_NO_ERROR: errCode = &quot;GL_NO_ERROR&quot;; break;\n        case GL_INVALID_ENUM: errCode = &quot;GL_INVALID_ENUM&quot;; break;\n        case GL_INVALID_VALUE: errCode = &quot;GL_INVALID_VALUE&quot;; break;\n        case GL_INVALID_OPERATION: errCode = &quot;GL_INVALID_OPERATION&quot;; break;\n        case GL_INVALID_FRAMEBUFFER_OPERATION: errCode = &quot;GL_INVALID_FRAMEBUFFER_OPERATION&quot;; break;\n        case GL_OUT_OF_MEMORY: errCode = &quot;GL_OUT_OF_MEMORY&quot;; break;\n        default: errCode = &quot;??&quot;; break;\n    }\n    mylog(&quot;GL ERROR %d-%s in %s\\n&quot;, res, errCode.c_str(), ref.c_str());\n    return -1;\n}\nvoid mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3) {\n    vec4 v2;\n    if (vOut == vIn) {\n        memcpy(&amp;v2, vIn, sizeof(vec4));\n        vIn = v2;\n    }\n    vIn&#91;3] = (float)v3;\n    mat4x4_mul_vec4(vOut, M, vIn);\n}\nvoid v3set(float* vOut, float x, float y, float z) {\n    vOut&#91;0] = x;\n    vOut&#91;1] = y;\n    vOut&#91;2] = z;\n}\nvoid v3copy(float* vOut, float* vIn) {\n    for (int i = 0; i &lt; 3; i++)\n        vOut&#91;i] = vIn&#91;i];\n}\nfloat v3yawRd(float* vIn) {\n    return atan2f(vIn&#91;0], vIn&#91;2]);\n}\nfloat v3pitchRd(float* vIn) {\n    return -atan2f(vIn&#91;1], sqrtf(vIn&#91;0] * vIn&#91;0] + vIn&#91;2] * vIn&#91;2]));\n}\nfloat v3pitchDg(float* vIn) { \n    return v3pitchRd(vIn) * radians2degrees; \n}\nfloat v3yawDg(float* vIn) { \n    return v3yawRd(vIn) * radians2degrees; \n}\n\nlong long int getSystemMillis() {\n    auto currentTime = std::chrono::system_clock::now().time_since_epoch();\n    return std::chrono::duration_cast&lt;std::chrono::milliseconds&gt;(currentTime).count();\n}\nlong long int getSystemNanos() {\n    auto currentTime = std::chrono::system_clock::now().time_since_epoch();\n    return std::chrono::duration_cast&lt;std::chrono::nanoseconds&gt;(currentTime).count();\n}\nint randomCallN = 0;\nint getRandom() {\n    if (randomCallN % 1000 == 0)\n        srand((unsigned int)getSystemNanos()); \/\/re-initialize random seed:\n    randomCallN++;\n    return rand();\n}\nint getRandom(int fromN, int toN) {\n    int randomN = getRandom();\n    int range = toN - fromN + 1;\n    return (fromN + randomN % range);\n}\nfloat getRandom(float fromN, float toN) {\n    int randomN = getRandom();\n    float range = toN - fromN;\n    return (fromN + (float)randomN \/ RAND_MAX * range);\n}\nstd::vector&lt;std::string&gt; splitString(std::string inString, std::string delimiter) {\n    std::vector&lt;std::string&gt; outStrings;\n    int delimiterSize = delimiter.size();\n    outStrings.clear();\n    while (inString.size() &gt; 0) {\n        int delimiterPosition = inString.find(delimiter);\n        if (delimiterPosition == 0) {\n            inString = inString.substr(delimiterSize, inString.size() - delimiterSize);\n            continue;\n        }\n        if (delimiterPosition == std::string::npos) {\n            \/\/last element\n            outStrings.push_back(trimString(inString));\n            break;\n        }\n        std::string outString = inString.substr(0, delimiterPosition);\n        outStrings.push_back(trimString(outString));\n        int startAt = delimiterPosition + delimiterSize;\n        inString = inString.substr(startAt, inString.size() - startAt);\n    }\n    return outStrings;\n}\nstd::string trimString(std::string inString) {\n    \/\/Remove leading and trailing spaces\n    int startsAt = inString.find_first_not_of(&quot; &quot;);\n    if (startsAt == std::string::npos)\n        return &quot;&quot;;\n    int endsAt = inString.find_last_not_of(&quot; &quot;) + 1;\n    return inString.substr(startsAt, endsAt - startsAt);\n}\nbool fileExists(const char* filePath) {\n    struct stat info;\n    if (stat(filePath, &amp;info) == 0)\n        return true;\n    else\n        return false;\n}\nstd::string getFullPath(std::string filePath) {\n    if (filePath.find(filesRoot) == 0)\n        return filePath;\n    else\n        return (filesRoot + filePath);\n}\nstd::string getInAppPath(std::string filePath) {\n    std::string inAppPath(filePath);\n    if (inAppPath.find(filesRoot) == 0) {\n        int startsAt = filesRoot.size();\n        inAppPath = inAppPath.substr(startsAt, inAppPath.size() - startsAt);\n    }\n    if (inAppPath.find(&quot;.&quot;) != std::string::npos) {\n        \/\/cut off file name\n        int endsAt = inAppPath.find_last_of(&quot;\/&quot;);\n        inAppPath = inAppPath.substr(0, endsAt + 1);\n    }\n    return inAppPath;\n}\nint makeDirs(std::string filePath) {\n    filePath = getFullPath(filePath);\n    std::string inAppPath = getInAppPath(filePath);\n    std::vector&lt;std::string&gt; path = splitString(inAppPath, &quot;\/&quot;);\n    int pathSize = path.size();\n    filePath.assign(filesRoot);\n    for (int i = 0; i &lt; pathSize; i++) {\n        filePath.append(&quot;\/&quot; + path.at(i));\n        if (fileExists(filePath.c_str())) {\n            continue;\n        }\n        \/\/create dir\n        myMkDir(filePath.c_str());\n        mylog(&quot;Folder %d: %s created.\\n&quot;, i, filePath.c_str());\n    }\n    return 1;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>So, <strong>other useful functions<\/strong> are:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><em>getRandom()<\/em> &#8211; soon we will need random numbers. C++ has a <em>rand()<\/em> function returning a pseudo-random integer in a range from 0 to RAND_MAX. For our needs we will wrap it in a couple of more handy functions, returning random <em>int <\/em>or <em>float <\/em>in a given range.<\/li><li><em>getFullPath()<\/em> and <em>getInAppPath()<\/em> from <em>FileLoader<\/em><\/li><li>Couple string functions: <em>splitString(\u2026)<\/em> and <em>trimString(\u2026)<\/em><\/li><li>Couple file functions: verification if <em>fileExists(\u2026)<\/em> and <em>makeDirs(\u2026)<\/em><\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>6. Build and run.<\/p>\n\n\n\n<p>Rotation speed looks the same as before. So, the goal is achieved.<\/p>\n\n\n\n<p>Checked on Android too, animation is perfectly smooth, so 30 FPS was a good choice.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n","protected":false},"excerpt":{"rendered":"<p class=\"mb-2\">Right now on both my devices (Android and PC) the box makes 1 revolution in approximately 4 seconds. It&#8217;s 360 frames (rotation speed is set to 1 degree per frame). This means 360\/4=90 frames per second (FPS). Obviously, on another devices it can be different. Besides, speed can depend on background processes and other factors. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-744","post","type-post","status-publish","format-standard","hentry","category-cross-platform-3d"],"_links":{"self":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/744","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/comments?post=744"}],"version-history":[{"count":18,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/744\/revisions"}],"predecessor-version":[{"id":1410,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/744\/revisions\/1410"}],"wp:attachment":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/media?parent=744"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/categories?post=744"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/tags?post=744"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}