win32/game_of_life Windows

Code

game_of_life.odin ¶
431 linesSource

1package game_of_life
2
3/*********************************************************************
4                            GAME  OF  LIFE
5                            (using win32)
6
7This example shows a simple setup of Game of Life using Win32. It uses
8Software Rendering. This is done by drawing the game cells into a
9texture.
10
11Controls:
12ESC - Quit
13P   - Toggle Pause
14R   - Restart
15H   - Toggle Help
16
17Build with:
18
19odin build . -resource:game_of_life.rc
20
21If you get errors during build about missing `winres.h`, either:
22- Remove the `-resource:game_of_life.rc` flag
23- Launch a "x64 Native Tools Command Prompt" and build from that
24  command prompt.
25
26**********************************************************************/
27
28import "base:intrinsics"
29import "base:runtime"
30import "core:fmt"
31import "core:math/rand"
32import "core:os"
33import win32 "core:sys/windows"
34import "core:time"
35
36// aliases
37Color :: [4]u8
38Int2 :: [2]i32
39
40// constants
41TITLE :: "Game of Life"
42CLASS_NAME :: "OdinMainClass"
43WORLD_SIZE :: Int2 {128, 64}
44ZOOM :: 12 // pixel size
45FPS :: 20
46
47BLACK :: Color{0, 0, 0, 255}
48WHITE :: Color{255, 255, 255, 255}
49
50HELP_TEXT :: `ESC - Quit
51P - Toggle Pause
52R - Restart
53H - Toggle Help`
54HELP_RECT :: win32.RECT {20, 20, 160, 105}
55show_help := true
56
57// The timer used for ticking the game.
58IDT_TIMER1 :: 10001
59
60COLOR_BITS :: 1
61PALETTE_COUNT :: 1 << COLOR_BITS
62Color_Palette :: [PALETTE_COUNT]Color
63
64Bitmap_Info :: struct {
65	bmiHeader: win32.BITMAPINFOHEADER,
66	bmiColors: Color_Palette,
67}
68
69Screen_Buffer :: [^]u8
70
71Config_Flag :: enum u32 {
72	CENTER = 1,
73}
74Config_Flags :: distinct bit_set[Config_Flag;u32]
75
76Window :: struct {
77	name:          win32.wstring,
78	size:          Int2,
79	fps:           i32,
80	control_flags: Config_Flags,
81}
82
83Game :: struct {
84	tick_rate:         time.Duration,
85	last_tick:         time.Time,
86	pause:             bool,
87	colors:            Color_Palette,
88	size:              Int2,
89	zoom:              i32,
90	world, next_world: ^World,
91	timer_id:          win32.UINT_PTR,
92	tick:              u32,
93	hbitmap:           win32.HBITMAP,
94	pvBits:            Screen_Buffer,
95	window:            Window,
96}
97
98World :: struct {
99	width:  i32,
100	height: i32,
101	alive:  []u8,
102}
103
104Cell :: struct {
105	width:  f32,
106	height: f32,
107}
108
109
110/*
111 Game Of Life rules:
112 * (1) A cell with 2 alive neighbors stays alive/dead
113 * (2) A cell with 3 alive neighbors stays/becomes alive
114 * (3) Otherwise: the cell dies/stays dead.
115
116 reads from world, writes into next_world
117*/
118update_world :: #force_inline proc(world: ^World, next_world: ^World) {
119	for x: i32 = 0; x < world.width; x += 1 {
120		for y: i32 = 0; y < world.height; y += 1 {
121			neighbors := count_neighbors(world, x, y)
122			index := y * world.width + x
123
124			switch neighbors {
125			case 2: next_world.alive[index] = world.alive[index]
126			case 3: next_world.alive[index] = 1
127			case:   next_world.alive[index] = 0
128			}
129		}
130	}
131}
132
133/*
134 Just a branch-less version of adding adding all neighbors together
135*/
136count_neighbors :: #force_inline proc(w: ^World, x: i32, y: i32) -> u8 {
137	// our world is a torus!
138	left  := (x - 1) %% w.width
139	right := (x + 1) %% w.width
140	up    := (y - 1) %% w.height
141	down  := (y + 1) %% w.height
142
143	top_left     := w.alive[up   * w.width + left ]
144	top          := w.alive[up   * w.width + x    ]
145	top_right    := w.alive[up   * w.width + right]
146
147	mid_left     := w.alive[y    * w.width + left ]
148	mid_right    := w.alive[y    * w.width + right]
149
150	bottom_left  := w.alive[down * w.width + left ]
151	bottom       := w.alive[down * w.width + x    ]
152	bottom_right := w.alive[down * w.width + right]
153
154	top_row    := top_left    + top     + top_right
155	mid_row    := mid_left              + mid_right
156	bottom_row := bottom_left + bottom  + bottom_right
157
158	total      := top_row     + mid_row + bottom_row
159	return total
160}
161
162/*
163 draws all the tiles of world.
164*/
165draw_world :: #force_inline proc(app: ^Game) {
166	cnt := int(app.world.width * app.world.height)
167	runtime.mem_copy(app.pvBits, &app.world.alive[0], cnt)
168}
169
170message_box :: #force_inline proc(text, caption: string, loc := #caller_location) {
171	win32.MessageBoxW(nil, win32.utf8_to_wstring(text), win32.utf8_to_wstring(caption), win32.MB_ICONSTOP | win32.MB_OK)
172}
173
174show_error_and_panic :: proc(msg: string, loc := #caller_location) {
175	message_box(fmt.tprintf("%s\nLast error: %x\n", msg, win32.GetLastError()), "Panic")
176	panic(msg, loc = loc)
177}
178
179get_rect_size :: #force_inline proc(rect: ^win32.RECT) -> Int2 {return {(rect.right - rect.left), (rect.bottom - rect.top)}}
180
181set_app :: #force_inline proc(hwnd: win32.HWND, app: ^Game) {win32.SetWindowLongPtrW(hwnd, win32.GWLP_USERDATA, win32.LONG_PTR(uintptr(app)))}
182
183get_app :: #force_inline proc(hwnd: win32.HWND) -> ^Game {return (^Game)(rawptr(uintptr(win32.GetWindowLongPtrW(hwnd, win32.GWLP_USERDATA))))}
184
185randomize_world :: proc(world: ^World) {
186	cc := world.width * world.height
187	for i in 0 ..< cc {world.alive[i] = u8(rand.int31_max(2))}
188}
189
190WM_CREATE :: proc(hwnd: win32.HWND, lparam: win32.LPARAM) -> win32.LRESULT {
191	pcs := (^win32.CREATESTRUCTW)(rawptr(uintptr(lparam)))
192	if pcs == nil {show_error_and_panic("lparam is nil")}
193	app := (^Game)(pcs.lpCreateParams)
194	if app == nil {show_error_and_panic("lpCreateParams is nil")}
195	set_app(hwnd, app)
196
197	app.timer_id = win32.SetTimer(hwnd, IDT_TIMER1, 1000 / FPS, nil)
198	if app.timer_id == 0 {show_error_and_panic("No timer")}
199
200	hdc := win32.GetDC(hwnd)
201	defer win32.ReleaseDC(hwnd, hdc)
202
203	bitmap_info := Bitmap_Info {
204		bmiHeader = win32.BITMAPINFOHEADER {
205			biSize        = size_of(win32.BITMAPINFOHEADER),
206			biWidth       = app.size.x,
207			biHeight      = -app.size.y, // minus for top-down
208			biPlanes      = 1,
209			biBitCount    = 8,
210			biCompression = win32.BI_RGB,
211			biClrUsed     = len(app.colors),
212		},
213		bmiColors = app.colors,
214	}
215	app.hbitmap = win32.CreateDIBSection(hdc, cast(^win32.BITMAPINFO)&bitmap_info, win32.DIB_RGB_COLORS, (^rawptr)(&app.pvBits), nil, 0)
216
217	if app.world != nil {
218		randomize_world(app.world)
219	}
220	app.pause = false
221
222	return 0
223}
224
225WM_DESTROY :: proc(hwnd: win32.HWND) -> win32.LRESULT {
226	app := get_app(hwnd)
227	if app == nil {show_error_and_panic("Missing app!")}
228	if app.timer_id != 0 {
229		if !win32.KillTimer(hwnd, app.timer_id) {
230			message_box("Unable to kill timer", "Error")
231		}
232		app.timer_id = 0
233	}
234	if app.hbitmap != nil {
235		if !win32.DeleteObject(win32.HGDIOBJ(app.hbitmap)) {
236			message_box("Unable to delete hbitmap", "Error")
237		}
238		app.hbitmap = nil
239	}
240	exit_code :: 0
241	win32.PostQuitMessage(exit_code)
242	return 0
243}
244
245WM_PAINT :: proc(hwnd: win32.HWND) -> win32.LRESULT {
246	app := get_app(hwnd)
247	if app == nil {return 0}
248
249	ps: win32.PAINTSTRUCT
250	hdc := win32.BeginPaint(hwnd, &ps)
251	defer win32.EndPaint(hwnd, &ps)
252
253	if app.hbitmap != nil {
254		hdc_source := win32.CreateCompatibleDC(hdc)
255		defer win32.DeleteDC(hdc_source)
256
257		win32.SelectObject(hdc_source, win32.HGDIOBJ(app.hbitmap))
258		client_size := get_rect_size(&ps.rcPaint)
259		win32.StretchBlt(hdc, 0, 0, client_size.x, client_size.y, hdc_source, 0, 0, app.size.x, app.size.y, win32.SRCCOPY)
260	}
261
262	if show_help {
263		rect := HELP_RECT
264		win32.RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20)
265		win32.InflateRect(&rect, -10, -10)
266		win32.DrawTextW(hdc, HELP_TEXT, -1, &rect, .DT_TOP)
267	}
268
269	return 0
270}
271
272WM_TIMER :: proc(hwnd: win32.HWND, wparam: win32.WPARAM, lparam: win32.LPARAM) -> win32.LRESULT {
273	switch wparam {
274	case IDT_TIMER1:
275		app := get_app(hwnd)
276		if app != nil {
277
278			if app.tick == 40 && show_help {show_help = false}
279
280			if !app.pause {
281				app.tick += 1
282				update_world(app.world, app.next_world)
283				app.world, app.next_world = app.next_world, app.world
284			}
285
286			draw_world(app)
287
288			win32.SetWindowTextW(hwnd, win32.utf8_to_wstring(fmt.tprintf("%s - %d\n", app.window.name, app.tick)))
289			win32.RedrawWindow(hwnd, nil, nil, .RDW_INVALIDATE | .RDW_UPDATENOW)
290		}
291	case:
292		fmt.printf("WM_TIMER %v %v %v\n", hwnd, wparam, lparam)
293	}
294	return 0
295}
296
297WM_CHAR :: proc(hwnd: win32.HWND, wparam: win32.WPARAM, lparam: win32.LPARAM) -> win32.LRESULT {
298	switch wparam {
299	case '\x1b':
300		win32.PostMessageW(hwnd, win32.WM_CLOSE, 0, 0)
301	case 'h':
302		show_help ~= true
303	case 'p':
304		app := get_app(hwnd)
305		app.pause ~= true
306		if !app.pause {show_help = false}
307	case 'r':
308		app := get_app(hwnd)
309		randomize_world(app.world)
310	case ' ':
311		app := get_app(hwnd)
312		siz := app.world.width * app.world.height
313		idx := rand.int31_max(siz)
314		app.world.alive[idx] = 1
315	}
316	return 0
317}
318
319wndproc :: proc "system" (hwnd: win32.HWND, msg: win32.UINT, wparam: win32.WPARAM, lparam: win32.LPARAM) -> win32.LRESULT {
320	context = runtime.default_context()
321	switch msg {
322	case win32.WM_CREATE:     return WM_CREATE(hwnd, lparam)
323	case win32.WM_DESTROY:    return WM_DESTROY(hwnd)
324	case win32.WM_ERASEBKGND: return 1 // paint should fill out the client area so no need to erase the background
325	case win32.WM_PAINT:      return WM_PAINT(hwnd)
326	case win32.WM_CHAR:       return WM_CHAR(hwnd, wparam, lparam)
327	case win32.WM_TIMER:      return WM_TIMER(hwnd, wparam, lparam)
328	case:                     return win32.DefWindowProcW(hwnd, msg, wparam, lparam)
329	}
330}
331
332register_class :: proc(instance: win32.HINSTANCE) -> win32.ATOM {
333	icon: win32.HICON = win32.LoadIconW(instance, cstring16(win32.MAKEINTRESOURCEW(101)))
334	if icon == nil {icon = win32.LoadIconW(nil, win32.wstring(win32._IDI_APPLICATION))}
335	if icon == nil {show_error_and_panic("Missing icon")}
336	cursor := win32.LoadCursorW(nil, win32.wstring(win32._IDC_ARROW))
337	if cursor == nil {show_error_and_panic("Missing cursor")}
338	wcx := win32.WNDCLASSW {
339		style         = win32.CS_HREDRAW | win32.CS_VREDRAW | win32.CS_OWNDC,
340		lpfnWndProc   = wndproc,
341		hInstance     = instance,
342		hIcon         = icon,
343		hCursor       = cursor,
344		lpszClassName = CLASS_NAME,
345	}
346	return win32.RegisterClassW(&wcx)
347}
348
349unregister_class :: proc(atom: win32.ATOM, instance: win32.HINSTANCE) {
350	if atom == 0 {show_error_and_panic("atom is zero")}
351	if !win32.UnregisterClassW(CLASS_NAME, instance) {show_error_and_panic("UnregisterClassW")}
352}
353
354adjust_size_for_style :: proc(size: ^Int2, dwStyle: win32.DWORD) {
355	rect := win32.RECT{0, 0, size.x, size.y}
356	if win32.AdjustWindowRect(&rect, dwStyle, false) {
357		size^ = {i32(rect.right - rect.left), i32(rect.bottom - rect.top)}
358	}
359}
360
361center_window :: proc(position: ^Int2, size: Int2) {
362	if deviceMode: win32.DEVMODEW; win32.EnumDisplaySettingsW(nil, win32.ENUM_CURRENT_SETTINGS, &deviceMode) {
363		device_size := Int2{i32(deviceMode.dmPelsWidth), i32(deviceMode.dmPelsHeight)}
364		position^ = (device_size - size) / 2
365	}
366}
367
368create_window :: #force_inline proc(instance: win32.HINSTANCE, atom: win32.ATOM, game: ^Game) -> win32.HWND {
369	if atom == 0 {show_error_and_panic("atom is zero")}
370	style :: win32.WS_OVERLAPPED | win32.WS_CAPTION | win32.WS_SYSMENU
371	size := game.window.size
372	pos := Int2{i32(win32.CW_USEDEFAULT), i32(win32.CW_USEDEFAULT)}
373	adjust_size_for_style(&size, style)
374	if .CENTER in game.window.control_flags {
375		center_window(&pos, size)
376	}
377	return win32.CreateWindowW(CLASS_NAME, game.window.name, style, pos.x, pos.y, size.x, size.y, nil, nil, instance, game)
378}
379
380message_loop :: proc() -> int {
381	msg: win32.MSG
382	for win32.GetMessageW(&msg, nil, 0, 0) > 0 {
383		win32.TranslateMessage(&msg)
384		win32.DispatchMessageW(&msg)
385	}
386	return int(msg.wParam)
387}
388
389run :: proc() -> int {
390	game := Game {
391		tick_rate = 300 * time.Millisecond,
392		last_tick = time.now(),
393		pause = true,
394		colors = {BLACK, WHITE},
395		size = WORLD_SIZE,
396		zoom = ZOOM,
397		window = Window{name = TITLE, size = WORLD_SIZE * ZOOM, fps = FPS, control_flags = {.CENTER}},
398	}
399	for i in 0 ..< PALETTE_COUNT {
400		c := u8((255 * int(i)) / (PALETTE_COUNT - 1))
401		game.colors[i] = {c, c, c, 255}
402	}
403
404	game.colors[0] = {128, 64, 32, 255}
405	game.colors[1] = {255, 128, 64, 255}
406
407	world := World{game.size.x, game.size.y, make([]u8, game.size.x * game.size.y)}
408	next_world := World{game.size.x, game.size.y, make([]u8, game.size.x * game.size.y)}
409	defer delete(world.alive)
410	defer delete(next_world.alive)
411	game.world = &world
412	game.next_world = &next_world
413
414	instance := win32.HINSTANCE(win32.GetModuleHandleW(nil))
415	if (instance == nil) {show_error_and_panic("No instance")}
416	atom := register_class(instance)
417	if atom == 0 {show_error_and_panic("Failed to register window class")}
418	defer unregister_class(atom, instance)
419
420	hwnd := create_window(instance, atom, &game)
421	if hwnd == nil {show_error_and_panic("Failed to create window")}
422	win32.ShowWindow(hwnd, win32.SW_SHOWDEFAULT)
423	win32.UpdateWindow(hwnd)
424
425	return message_loop()
426}
427
428main :: proc() {
429	exit_code := run()
430	os.exit(exit_code)
431}

Other Files

game_of_life.rc ¶
46 linesSource


#include "winres.h"

LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_UK
#pragma code_page(1252)

#define IDI_ICON1                       101

#define Q(x) #x
#define QUOTE(x) Q(x)

VS_VERSION_INFO VERSIONINFO
 FILEVERSION 0,1,0,0
 PRODUCTVERSION 0,1,0,0
 FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
 FILEFLAGS 0x1L
#else
 FILEFLAGS 0x0L
#endif
 FILEOS 0x40004L
 FILETYPE 0x1L
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "080904b0"
        BEGIN
            VALUE "CompanyName", "https://github.com/odin-lang/examples/"
            VALUE "FileDescription", "Game of Life"
            VALUE "FileVersion", "0.1.0.0"
            VALUE "InternalName", "game_of_life.exe"
            VALUE "LegalCopyright", "Copyright (c) 2024"
            VALUE "OriginalFilename", "game_of_life.exe"
            VALUE "ProductName", "Odin Examples - Game of Life"
            VALUE "ProductVersion", "0.1.0.0"
            VALUE "Comments", "This example shows a simple setup for a game with Input processing, updating game state and drawing game state to the screen using Win32."
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x809, 1200
    END
END

IDI_ICON1   ICON    "..\\emblem.ico"

Declarations Used 93