ภาพวาดบนผืนผ้าใบในรูปแบบ Emscripten

ดูวิธีแสดงผลกราฟิก 2 มิติบนเว็บจาก WebAssembly ด้วย Emscripten

ระบบปฏิบัติการที่แตกต่างกันมี API ที่ต่างกันสำหรับการวาดกราฟิก ความแตกต่างนี้ยิ่งสับสนมากขึ้นเมื่อเขียนโค้ดข้ามแพลตฟอร์มหรือพอร์ตกราฟิกจากระบบหนึ่งไปยังอีกระบบหนึ่ง ซึ่งรวมถึงเมื่อย้ายโค้ดแบบเนทีฟไปยัง WebAssembly

ในโพสต์นี้คุณจะได้เรียนรู้วิธีการ 2 วิธีในการวาดกราฟิก 2 มิติลงในองค์ประกอบ Canvas ในเว็บจากโค้ด C หรือ C++ ที่คอมไพล์ด้วย Emscripten

Canvas ผ่าน Embind

หากจะเริ่มโปรเจ็กต์ใหม่แทนที่จะพยายามพอร์ตโปรเจ็กต์ที่มีอยู่ การใช้ HTML Canvas API ผ่านระบบการเชื่อมโยง Embind ของ Emscripten เป็นวิธีที่ง่ายที่สุดในการย้ายโปรเจ็กต์ใหม่ Embind ให้คุณดำเนินการกับค่า JavaScript ที่กำหนดเองได้โดยตรง

เพื่อทำความเข้าใจวิธีใช้ Embind ก่อนอื่นให้ดูตัวอย่างจาก MDN ต่อไปนี้ซึ่งพบองค์ประกอบ <canvas> แล้ววาดรูปร่างต่างๆ ลงบนองค์ประกอบ

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 150, 100);

วิธีทับศัพท์เป็น C++ ด้วย Embind มีดังนี้

#include <emscripten/val.h>

using emscripten::val;

// Use thread_local when you want to retrieve & cache a global JS variable once per thread.
thread_local const val document = val::global("document");

// …

int main() {
  val canvas = document.call<val>("getElementById", "canvas");
  val ctx = canvas.call<val>("getContext", "2d");
  ctx.set("fillStyle", "green");
  ctx.call<void>("fillRect", 10, 10, 150, 100);
}

เมื่อลิงก์โค้ดนี้ อย่าลืมส่ง --bind เพื่อเปิดใช้ Embind:

emcc --bind example.cpp -o example.html

จากนั้นแสดงเนื้อหาที่คอมไพล์ด้วยเซิร์ฟเวอร์แบบคงที่ และโหลดตัวอย่างในเบราว์เซอร์ได้โดยทำดังนี้

หน้า HTML ที่สร้างด้วย Emscripten แสดงสี่เหลี่ยมผืนผ้าสีเขียวบนผืนผ้าใบสีดำ

การเลือกองค์ประกอบ Canvas

เมื่อใช้ Shell HTML ที่สร้างโดย Emscripten กับคำสั่ง Shell ก่อนหน้า ระบบจะรวม Canvas แล้วตั้งค่าให้คุณ คุณสามารถสร้างการสาธิตและตัวอย่างง่ายๆ ได้ง่ายขึ้น แต่ในแอปพลิเคชันขนาดใหญ่ คุณควรรวม JavaScript และ WebAssembly ที่สร้างโดย Emscripten ไว้ในหน้า HTML ที่คุณออกแบบเอง

โค้ด JavaScript ที่สร้างขึ้นจะค้นหาองค์ประกอบ Canvas ที่จัดเก็บไว้ในพร็อพเพอร์ตี้ Module.canvas คุณจะตั้งค่าในระหว่างการเริ่มต้นได้ เช่นเดียวกับพร็อพเพอร์ตี้โมดูลอื่นๆ

หากคุณกำลังใช้โหมด ES6 (ตั้งค่าเอาต์พุตไปยังเส้นทางที่มีส่วนขยาย .mjs หรือใช้การตั้งค่า -s EXPORT_ES6) คุณสามารถส่ง Canvas ได้ดังนี้

import initModule from './emscripten-generated.mjs';

const Module = await initModule({
  canvas: document.getElementById('my-canvas')
});

หากใช้เอาต์พุตสคริปต์ปกติ คุณต้องประกาศออบเจ็กต์ Module ก่อนที่จะโหลดไฟล์ JavaScript ที่สร้างโดย Emscripten ดังนี้

<script>
var Module = {
  canvas: document.getElementById('my-canvas')
};
</script>
<script src="emscripten-generated.js"></script>

OpenGL และ SDL2

OpenGL เป็น API ข้ามแพลตฟอร์มที่ได้รับความนิยมสำหรับกราฟิกของคอมพิวเตอร์ เมื่อใช้ใน Emscripten ระบบจะช่วยแปลงชุดย่อยของการดำเนินการ OpenGL ที่รองรับเป็น WebGL หากแอปพลิเคชันต้องใช้ฟีเจอร์ที่รองรับใน OpenGL ES 2.0 หรือ 3.0 แต่ไม่ใช่ใน WebGL Emscripten จะช่วยจำลองฟีเจอร์เหล่านั้นได้เช่นกัน แต่คุณต้องเลือกใช้ผ่านการตั้งค่าที่เกี่ยวข้อง

คุณสามารถใช้ OpenGL โดยตรงหรือผ่านไลบรารีกราฟิก 2 มิติและ 3 มิติระดับสูงขึ้น โดยมีเครื่องมือ 2 อย่างที่ถ่ายโอนไปยังเว็บด้วย Emscripten ในโพสต์นี้ ฉันเน้นไปที่กราฟิก 2 มิติ และ SDL2 เป็นไลบรารีที่แนะนำสำหรับในตอนนี้ เนื่องจากผ่านการทดสอบมาเป็นอย่างดีและรองรับอัปสตรีมของแบ็กเอนด์ Emscripten อย่างเป็นทางการ

การวาดสี่เหลี่ยมผืนผ้า

ส่วน "เกี่ยวกับ SDL" ในเว็บไซต์อย่างเป็นทางการระบุว่า

Simple DirectMedia Layer เป็นไลบรารีการพัฒนาข้ามแพลตฟอร์มที่ออกแบบมาเพื่อให้ผู้ใช้สามารถเข้าถึงเสียง แป้นพิมพ์ เมาส์ จอยสติ๊ก และฮาร์ดแวร์กราฟิกผ่าน OpenGL และ Direct3D ในระดับต่ำ

ฟีเจอร์เหล่านั้นทั้งหมด ไม่ว่าจะเป็นการควบคุมเสียง แป้นพิมพ์ เมาส์ และกราฟิก ก็ได้รับการย้ายตำแหน่งและใช้งานกับ Emscripten บนเว็บได้ด้วย คุณจึงนำเกมทั้งเกมที่สร้างด้วย SDL2 มาใช้งานได้ไม่ยุ่งยากมากนัก หากจะโอนโปรเจ็กต์ที่มีอยู่ โปรดดูส่วน "การผสานรวมกับระบบบิลด์" ของเอกสาร Emscripten

เพื่อความสะดวก ในโพสต์นี้เราจะเน้นเรื่องเคสแบบไฟล์เดียวและแปลตัวอย่างสี่เหลี่ยมผืนผ้าก่อนหน้านี้เป็น SDL2

#include <SDL2/SDL.h>

int main() {
  // Initialize SDL graphics subsystem.
  SDL_Init(SDL_INIT_VIDEO);

  // Initialize a 300x300 window and a renderer.
  SDL_Window *window;
  SDL_Renderer *renderer;
  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  // Set a color for drawing matching the earlier `ctx.fillStyle = "green"`.
  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  // Create and draw a rectangle like in the earlier `ctx.fillRect()`.
  SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
  SDL_RenderFillRect(renderer, &rect);

  // Render everything from a buffer to the actual screen.
  SDL_RenderPresent(renderer);

  // TODO: cleanup
}

เมื่อลิงก์กับ Emscripten คุณต้องใช้ -s USE_SDL=2 ซึ่งจะบอกให้ Emscripten ดึงข้อมูลไลบรารี SDL2 ซึ่งคอมไพล์ล่วงหน้าไปยัง WebAssembly แล้ว แล้วลิงก์กับแอปพลิเคชันหลัก

emcc example.cpp -o example.html -s USE_SDL=2

เมื่อโหลดตัวอย่างในเบราว์เซอร์ คุณจะเห็นสี่เหลี่ยมผืนผ้าสีเขียวที่คุ้นเคยดังนี้

หน้า HTML ที่สร้างด้วย Emscripten แสดงสี่เหลี่ยมผืนผ้าสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดำ

อย่างไรก็ตาม รหัสนี้มีปัญหา 2-3 ข้อ ประการแรกคือ ขาดการจัดการทรัพยากรที่จัดสรรอย่างเหมาะสม ประการที่ 2 บนเว็บ ระบบจะไม่ปิดหน้าเว็บโดยอัตโนมัติเมื่อแอปพลิเคชันทำงานเสร็จแล้ว ดังนั้นรูปภาพบนผืนผ้าใบจึงยังคงอยู่ อย่างไรก็ตาม เมื่อมีการคอมไพล์โค้ดเดียวกันอีกครั้งโดยค่าเริ่มต้นด้วย

clang example.cpp -o example -lSDL2

และทำงาน หน้าต่างที่สร้างจะกะพริบเพียงชั่วครู่และปิดทันทีเมื่อออก ดังนั้นผู้ใช้จึงไม่มีโอกาสเห็นภาพ

การผสานรวมการวนซ้ำเหตุการณ์

ตัวอย่างที่สมบูรณ์และเป็นสำนวนมากกว่าจะต้องรอในลูปเหตุการณ์จนกว่าผู้ใช้จะเลือกออกจากแอปพลิเคชัน

#include <SDL2/SDL.h>

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window *window;
  SDL_Renderer *renderer;
  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
  SDL_RenderFillRect(renderer, &rect);
  SDL_RenderPresent(renderer);

  while (1) {
    SDL_Event event;
    SDL_PollEvent(&event);
    if (event.type == SDL_QUIT) {
      break;
    }
  }

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

หลังจากวาดภาพลงในหน้าต่างแล้ว แอปพลิเคชันจะรอวนซ้ำและประมวลผลแป้นพิมพ์ เมาส์ และเหตุการณ์อื่นๆ ของผู้ใช้ได้ เมื่อปิดหน้าต่าง ผู้ใช้จะทริกเกอร์เหตุการณ์ SDL_QUIT ซึ่งจะถูกสกัดกั้นเพื่อออกจากลูป หลังจากสิ้นสุดการวนซ้ำ แอปพลิเคชันจะทำความสะอาดและออกจากการทำงานเอง

ตอนนี้การคอมไพล์ตัวอย่างนี้บน Linux ทำงานตามที่คาดไว้ และแสดงหน้าต่างขนาด 300 x 300 ที่มีสี่เหลี่ยมผืนผ้าสีเขียว:

หน้าต่าง Linux ทรงสี่เหลี่ยมจัตุรัสที่มีพื้นหลังสีดำและสี่เหลี่ยมผืนผ้าสีเขียว

แต่ตัวอย่างนี้ใช้ไม่ได้บนเว็บแล้ว หน้าที่สร้างขึ้นด้วย Emscripten ค้างทันทีในระหว่างการโหลดและจะไม่แสดงภาพที่แสดงผล

หน้า HTML ที่สร้างด้วย Emscripten ซ้อนทับกับกล่องโต้ตอบข้อผิดพลาด &quot;หน้าเว็บไม่ตอบสนอง&quot; แนะนำให้รอให้หน้าเว็บแสดงความรับผิดชอบหรือออกจากหน้า

เกิดอะไรขึ้น ผมจะอ้างอิงคำตอบจากบทความ "การใช้ API ของเว็บแบบอะซิงโครนัสจาก WebAssembly"

เวอร์ชันสั้นๆ คือเบราว์เซอร์จะเรียกใช้โค้ดทั้งหมดในลักษณะการวนซ้ำที่ไม่มีที่สิ้นสุด โดยนำโค้ดออกจากคิวทีละรายการ เมื่อมีการทริกเกอร์บางเหตุการณ์ เบราว์เซอร์จะจัดคิวตัวจัดการที่เกี่ยวข้อง และในการทำซ้ำรอบถัดไป เบราว์เซอร์จะออกจากคิวและดำเนินการ กลไกนี้ช่วยให้จำลองการเกิดขึ้นพร้อมกันและเรียกใช้การดำเนินการพร้อมกันจำนวนมากในขณะที่ใช้เทรดเดียวเท่านั้น

สิ่งสำคัญที่ต้องจำไว้เกี่ยวกับกลไกนี้คือขณะที่โค้ด JavaScript ที่กำหนดเอง (หรือ WebAssembly) กำลังทำงาน ลูปเหตุการณ์จะถูกบล็อก [...]

ตัวอย่างก่อนหน้านี้เรียกใช้การวนซ้ำของเหตุการณ์ที่ไม่สิ้นสุด ส่วนโค้ดจะทำงานภายในลูปเหตุการณ์ที่ไม่สิ้นสุดอีกอันหนึ่ง ซึ่งเบราว์เซอร์ระบุไว้โดยนัย ลูปด้านในจะไม่ลดการควบคุมไปให้กับชั้นนอก เบราว์เซอร์จึงไม่มีโอกาสประมวลผลเหตุการณ์ภายนอกหรือดึงสิ่งต่างๆ มาวางบนหน้าเว็บ

การแก้ปัญหานี้ทำได้ 2 วิธี

การเลิกบล็อกเหตุการณ์วนซ้ำด้วย Asyncify

ขั้นแรก คุณจะใช้ Asyncify ได้ตามที่อธิบายไว้ในบทความที่ลิงก์ เป็นฟีเจอร์ Emscripten ที่อนุญาตให้ "หยุด" โปรแกรม C หรือ C++ ชั่วคราว ให้การควบคุมกลับไปยังลูปเหตุการณ์ และปลุกระบบโปรแกรมเมื่อการทำงานแบบไม่พร้อมกันบางอย่างเสร็จสิ้น

การดำเนินการแบบอะซิงโครนัสดังกล่าวอาจเป็น "โหมดสลีปเป็นเวลาต่ำสุดที่เป็นไปได้" ซึ่งแสดงผ่าน emscripten_sleep(0) API การฝังโค้ดไว้ตรงกลางลูปจะช่วยให้ฉันมั่นใจได้ว่าระบบจะส่งตัวควบคุมกลับไปยัง Event Loop ของเบราว์เซอร์ในการทำซ้ำแต่ละครั้ง หน้าเว็บจะยังคงตอบสนองและรองรับเหตุการณ์ต่างๆ ได้ ดังนี้

#include <SDL2/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window *window;
  SDL_Renderer *renderer;
  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
  SDL_RenderFillRect(renderer, &rect);
  SDL_RenderPresent(renderer);

  while (1) {
    SDL_Event event;
    SDL_PollEvent(&event);
    if (event.type == SDL_QUIT) {
      break;
    }
#ifdef __EMSCRIPTEN__
    emscripten_sleep(0);
#endif
  }

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

ตอนนี้โค้ดนี้จะต้องได้รับการคอมไพล์โดยเปิดใช้ Asyncify ดังนี้

emcc example.cpp -o example.html -s USE_SDL=2 -s ASYNCIFY

และแอปพลิเคชันก็ทำงานได้ตามปกติบนเว็บอีกครั้ง:

หน้า HTML ที่สร้างด้วย Emscripten แสดงสี่เหลี่ยมผืนผ้าสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดำ

อย่างไรก็ตาม Asyncify อาจมีโอเวอร์เฮดของขนาดโค้ดที่ไม่สำคัญ หากใช้เฉพาะสำหรับการวนซ้ำเหตุการณ์ระดับบนสุดในแอปพลิเคชัน ตัวเลือกที่ดีกว่าคือใช้ฟังก์ชัน emscripten_set_main_loop ได้

การเลิกบล็อกเหตุการณ์วนซ้ำด้วย API "ลูปหลัก"

emscripten_set_main_loop ไม่ต้องใช้การเปลี่ยนรูปแบบคอมไพเลอร์ใดๆ เพื่อคลายเครียดและกรอกลับการเรียกใช้ชุดรายการ ซึ่งจะช่วยให้ไม่ต้องเสียเวลาไปกับโอเวอร์เฮดของขนาดโค้ด อย่างไรก็ตาม คุณจะต้องทำการแก้ไขโค้ดด้วยตนเองหลายครั้ง

ก่อนอื่น ต้องแยกส่วนเนื้อหาของลูปเหตุการณ์ลงในฟังก์ชันแยกต่างหาก จากนั้นต้องมีการเรียก emscripten_set_main_loop ด้วยฟังก์ชันนั้นเป็นโค้ดเรียกกลับในอาร์กิวเมนต์แรก, FPS ในอาร์กิวเมนต์ที่ 2 (0 สำหรับช่วงการรีเฟรชแบบเนทีฟ) และบูลีนที่ระบุว่าจะจำลองการวนซ้ำที่ไม่มีสิ้นสุด (true) ในอาร์กิวเมนต์ที่ 3 หรือไม่

emscripten_set_main_loop(callback, 0, true);

โค้ดเรียกกลับที่สร้างขึ้นใหม่จะไม่มีสิทธิ์เข้าถึงตัวแปรสแต็กในฟังก์ชัน main จึงต้องแยกตัวแปรอย่าง window และ renderer ลงในโครงสร้างที่จัดสรรฮีป และตัวชี้ของตัวแปรที่ส่งผ่านตัวแปร emscripten_set_main_loop_arg ของ API หรือแยกลงในตัวแปร static ส่วนกลาง (ฉันเลือกใช้ตัวแปรหลังเพื่อให้เรียบง่าย) ผลที่ได้ตามมายากขึ้นเล็กน้อย แต่ใช้สี่เหลี่ยมผืนผ้าเดียวกับตัวอย่างสุดท้าย:

#include <SDL2/SDL.h>
#include <stdio.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

SDL_Window *window;
SDL_Renderer *renderer;

bool handle_events() {
  SDL_Event event;
  SDL_PollEvent(&event);
  if (event.type == SDL_QUIT) {
    return false;
  }
  return true;
}

void run_main_loop() {
#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
  while (handle_events())
    ;
#endif
}

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
  SDL_RenderFillRect(renderer, &rect);
  SDL_RenderPresent(renderer);

  run_main_loop();

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

เนื่องจากการเปลี่ยนแปลงขั้นตอนการควบคุมทั้งหมดเป็นแบบดำเนินการด้วยตนเองและแสดงในซอร์สโค้ด จึงสามารถคอมไพล์ได้โดยไม่ต้องใช้ฟีเจอร์ Asyncify อีกครั้ง ดังนี้

emcc example.cpp -o example.html -s USE_SDL=2

ตัวอย่างนี้อาจดูไม่มีประโยชน์ เนื่องจากทำงานไม่ต่างจากเวอร์ชันแรกตรงที่วาดรูปสี่เหลี่ยมผืนผ้าบนผืนผ้าใบได้สำเร็จแม้ว่าโค้ดจะง่ายกว่ามาก และเหตุการณ์ SDL_QUIT ซึ่งเป็นเหตุการณ์เดียวที่จัดการในฟังก์ชัน handle_events จะไม่สนใจในเว็บ

อย่างไรก็ตาม การผสานรวมเหตุการณ์แบบวนซ้ำ (Event Loop) ที่เหมาะสมไม่ว่าจะผ่าน Asyncify หรือผ่าน emscripten_set_main_loop จะได้ผลหากคุณตัดสินใจเพิ่มภาพเคลื่อนไหวหรือการโต้ตอบประเภทใดก็ตาม

การจัดการการโต้ตอบของผู้ใช้

ตัวอย่างเช่น หากมีการเปลี่ยนแปลงเล็กน้อยในตัวอย่างล่าสุด คุณจะสามารถทำให้สี่เหลี่ยมขยับตามเหตุการณ์ของแป้นพิมพ์ได้ ดังนี้

#include <SDL2/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

SDL_Window *window;
SDL_Renderer *renderer;

SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};

void redraw() {
  SDL_SetRenderDrawColor(renderer, /* RGBA: black */ 0x00, 0x00, 0x00, 0xFF);
  SDL_RenderClear(renderer);
  SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_RenderFillRect(renderer, &rect);
  SDL_RenderPresent(renderer);
}

uint32_t ticksForNextKeyDown = 0;

bool handle_events() {
  SDL_Event event;
  SDL_PollEvent(&event);
  if (event.type == SDL_QUIT) {
    return false;
  }
  if (event.type == SDL_KEYDOWN) {
    uint32_t ticksNow = SDL_GetTicks();
    if (SDL_TICKS_PASSED(ticksNow, ticksForNextKeyDown)) {
      // Throttle keydown events for 10ms.
      ticksForNextKeyDown = ticksNow + 10;
      switch (event.key.keysym.sym) {
        case SDLK_UP:
          rect.y -= 1;
          break;
        case SDLK_DOWN:
          rect.y += 1;
          break;
        case SDLK_RIGHT:
          rect.x += 1;
          break;
        case SDLK_LEFT:
          rect.x -= 1;
          break;
      }
      redraw();
    }
  }
  return true;
}

void run_main_loop() {
#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
  while (handle_events())
    ;
#endif
}

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  redraw();
  run_main_loop();

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

การวาดรูปร่างอื่นๆ ด้วย SDL2_gfx

SDL2 ขจัดความแตกต่างข้ามแพลตฟอร์มและอุปกรณ์สื่อประเภทต่างๆ มาไว้ใน API เดียว แต่ยังคงเป็นไลบรารีในระดับต่ำ โดยเฉพาะอย่างยิ่งสำหรับกราฟิก แม้จะมี API สำหรับจุดวาด เส้น และสี่เหลี่ยมผืนผ้า แต่ผู้ใช้ยังคงสามารถใช้รูปทรงและการแปลงที่ซับซ้อนยิ่งขึ้น

โดย SDL2_gfx คือไลบรารีแยกต่างหากที่ช่วยเติมเต็มช่องว่างดังกล่าว เช่น สามารถใช้วงกลมแทนสี่เหลี่ยมผืนผ้าในตัวอย่างด้านบน

#include <SDL2/SDL.h>
#include <SDL2/SDL2_gfxPrimitives.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

SDL_Window *window;
SDL_Renderer *renderer;

SDL_Point center = {.x = 100, .y = 100};
const int radius = 100;

void redraw() {
  SDL_SetRenderDrawColor(renderer, /* RGBA: black */ 0x00, 0x00, 0x00, 0xFF);
  SDL_RenderClear(renderer);
  filledCircleRGBA(renderer, center.x, center.y, radius,
                   /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
  SDL_RenderPresent(renderer);
}

uint32_t ticksForNextKeyDown = 0;

bool handle_events() {
  SDL_Event event;
  SDL_PollEvent(&event);
  if (event.type == SDL_QUIT) {
    return false;
  }
  if (event.type == SDL_KEYDOWN) {
    uint32_t ticksNow = SDL_GetTicks();
    if (SDL_TICKS_PASSED(ticksNow, ticksForNextKeyDown)) {
      // Throttle keydown events for 10ms.
      ticksForNextKeyDown = ticksNow + 10;
      switch (event.key.keysym.sym) {
        case SDLK_UP:
          center.y -= 1;
          break;
        case SDLK_DOWN:
          center.y += 1;
          break;
        case SDLK_RIGHT:
          center.x += 1;
          break;
        case SDLK_LEFT:
          center.x -= 1;
          break;
      }
      redraw();
    }
  }
  return true;
}

void run_main_loop() {
#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
  while (handle_events())
    ;
#endif
}

int main() {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);

  redraw();
  run_main_loop();

  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);

  SDL_Quit();
}

ตอนนี้ไลบรารี SDL2_gfx จะต้องลิงก์กับแอปพลิเคชันด้วย ซึ่งดำเนินการคล้ายกับ SDL2:

# Native version
$ clang example.cpp -o example -lSDL2 -lSDL2_gfx
# Web version
$ emcc --bind foo.cpp -o foo.html -s USE_SDL=2 -s USE_SDL_GFX=2

และผลลัพธ์ที่ทำงานใน Linux มีดังนี้

หน้าต่าง Linux ทรงสี่เหลี่ยมจัตุรัสที่มีพื้นหลังสีดำและวงกลมสีเขียว

และบนเว็บ

หน้า HTML ที่สร้างด้วย Emscripten แสดงวงกลมสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดำ

สำหรับกราฟิกพื้นฐานเพิ่มเติม โปรดดูเอกสารที่สร้างขึ้นโดยอัตโนมัติ