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

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

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

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

หากคุณกำลังเริ่มต้นโปรเจ็กต์ใหม่แทนที่จะพอร์ตโปรเจ็กต์ที่มีอยู่ วิธีที่ง่ายที่สุดอาจเป็นการใช้ Canvas API ของ HTML ผ่านระบบการเชื่อมโยง 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

เมื่อใช้เชลล์ HTML ที่ Emscripten สร้างขึ้นกับคำสั่งเชลล์ก่อนหน้า ระบบจะรวมและตั้งค่า 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 อย่าง ประการแรก ไม่มีการล้างทรัพยากรที่จัดสรรอย่างเหมาะสม ประการที่ 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; ที่แนะนำให้รอให้หน้าเว็บกลับมาทำงานอีกครั้งหรือออกจากหน้า

เกิดอะไรขึ้น เราจะยกคำตอบจากบทความ"การใช้ Web API แบบไม่สอดคล้องจาก WebAssembly" ดังนี้

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

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

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

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

การปลดบล็อกลูปเหตุการณ์ด้วย Asyncify

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

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

#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 จะถูกละเว้นบนเว็บอยู่ดี

อย่างไรก็ตาม การผสานรวมลูปเหตุการณ์อย่างเหมาะสมผ่าน 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 สร้างขึ้นซึ่งแสดงวงกลมสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดํา

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