4 This is an SDL2 implementation of the game front end. It can be used to
5 compile a native executable or a transpiled JS browser version with
8 This frontend is not strictly minimal, it could be reduced a lot. If you want
9 a learning example of frontend, look at another, simpler one, e.g. terminal.
11 To compile with emscripten run:
13 emcc ./main_sdl.c -s USE_SDL=2 -O3 --shell-file HTMLshell.html -o game.html
15 by Miloslav Ciz (drummyfish), 2019
17 Released under CC0 1.0 (https://creativecommons.org/publicdomain/zero/1.0/)
18 plus a waiver of all other intellectual property. The goal of this work is to
19 be and remain completely in the public domain forever, available for any use
23 #if defined(_WIN32) || defined(WIN32) || defined(__WIN32__) || defined(__NT__) || defined(__APPLE__)
24 #define SFG_OS_IS_MALWARE 1
27 // #define SFG_START_LEVEL 1
28 // #define SFG_QUICK_WIN 1
29 // #define SFG_IMMORTAL 1
30 // #define SFG_ALL_LEVELS 1
31 // #define SFG_UNLOCK_DOOR 1
32 // #define SFG_REVEAL_MAP 1
33 // #define SFG_INFINITE_AMMO 1
34 // #define SFG_TIME_MULTIPLIER 512
35 // #define SFG_CPU_LOAD(percent) printf("CPU load: %d%\n",percent);
38 #ifndef __EMSCRIPTEN__
42 #define SFG_LOG(str) puts(str);
43 #define SFG_SCREEN_RESOLUTION_X 700
44 #define SFG_SCREEN_RESOLUTION_Y 512
45 #define SFG_DITHERED_SHADOW 1
46 #define SFG_DIMINISH_SPRITES 1
47 #define SFG_HEADBOB_SHEAR (-1 * SFG_SCREEN_RESOLUTION_Y / 80)
48 #define SFG_BACKGROUND_BLUR 1
52 #define SFG_SCREEN_RESOLUTION_X 640
53 #define SFG_SCREEN_RESOLUTION_Y 480
54 #define SFG_RAYCASTING_SUBSAMPLE 2
55 #define SFG_RESOLUTION_SCALEDOWN 2
56 #define SFG_LOG(str) puts(str);
57 #define SFG_DIMINISH_SPRITES 0
58 #define SFG_DITHERED_SHADOW 0
59 #define SFG_BACKGROUND_BLUR 0
60 #define SFG_RAYCASTING_MAX_STEPS 18
61 #define SFG_RAYCASTING_MAX_HITS 8
66 #define SFG_SCREEN_RESOLUTION_X 512
67 #define SFG_SCREEN_RESOLUTION_Y 320
68 #define SFG_CAN_EXIT 0
69 #define SFG_RESOLUTION_SCALEDOWN 2
70 #define SFG_DITHERED_SHADOW 1
71 #define SFG_BACKGROUND_BLUR 0
72 #define SFG_RAYCASTING_MAX_STEPS 18
73 #define SFG_RAYCASTING_MAX_HITS 8
75 #include <emscripten.h>
79 SDL is easier to play thanks to nice controls so make the player take full
80 damage to make it a bit harder.
82 #define SFG_PLAYER_DAMAGE_MULTIPLIER 1024
84 #define SDL_MUSIC_VOLUME 16
86 #define SDL_ANALOG_DIVIDER 1024
88 #if !SFG_OS_IS_MALWARE
92 #define SDL_MAIN_HANDLED 1
94 #define SDL_DISABLE_IMMINTRIN_H 1
103 const uint8_t *sdlKeyboardState
;
104 uint8_t webKeyboardState
[SFG_KEY_COUNT
];
105 uint8_t sdlMouseButtonState
= 0;
106 int8_t sdlMouseWheelState
= 0;
107 SDL_GameController
*sdlController
;
109 uint16_t sdlScreen
[SFG_SCREEN_RESOLUTION_X
* SFG_SCREEN_RESOLUTION_Y
]; // RGB565
112 SDL_Renderer
*renderer
;
113 SDL_Texture
*texture
;
114 SDL_Surface
*screenSurface
;
116 // now implement the Anarch API functions (SFG_*)
118 void SFG_setPixel(uint16_t x
, uint16_t y
, uint8_t colorIndex
)
120 sdlScreen
[y
* SFG_SCREEN_RESOLUTION_X
+ x
] = paletteRGB565
[colorIndex
];
123 uint32_t SFG_getTimeMs()
125 return SDL_GetTicks();
128 void SFG_save(uint8_t data
[SFG_SAVE_SIZE
])
130 FILE *f
= fopen(SFG_SAVE_FILE_PATH
,"wb");
132 puts("SDL: opening and writing save file");
136 puts("SDL: could not open the file!");
140 fwrite(data
,1,SFG_SAVE_SIZE
,f
);
145 uint8_t SFG_load(uint8_t data
[SFG_SAVE_SIZE
])
147 #ifndef __EMSCRIPTEN__
148 FILE *f
= fopen(SFG_SAVE_FILE_PATH
,"rb");
150 puts("SDL: opening and reading save file");
154 puts("SDL: no save file to open");
158 fread(data
,1,SFG_SAVE_SIZE
,f
);
164 // no saving for web version
169 void SFG_sleepMs(uint16_t timeMs
)
171 #ifndef __EMSCRIPTEN__
172 usleep(timeMs
* 1000);
176 #ifdef __EMSCRIPTEN__
177 void webButton(uint8_t key
, uint8_t down
) // HTML button pressed
179 webKeyboardState
[key
] = down
;
183 int8_t mouseMoved
= 0; /* Whether the mouse has moved since program started,
184 this is needed to fix an SDL limitation. */
186 void SFG_getMouseOffset(int16_t *x
, int16_t *y
)
188 #ifndef __EMSCRIPTEN__
193 SDL_GetMouseState(&mX
,&mY
);
195 *x
= mX
- SFG_SCREEN_RESOLUTION_X
/ 2;
196 *y
= mY
- SFG_SCREEN_RESOLUTION_Y
/ 2;
198 SDL_WarpMouseInWindow(window
,
199 SFG_SCREEN_RESOLUTION_X
/ 2, SFG_SCREEN_RESOLUTION_Y
/ 2);
202 if (sdlController
!= NULL
)
205 (SDL_GameControllerGetAxis(sdlController
,SDL_CONTROLLER_AXIS_RIGHTX
) +
206 SDL_GameControllerGetAxis(sdlController
,SDL_CONTROLLER_AXIS_LEFTX
)) /
210 (SDL_GameControllerGetAxis(sdlController
,SDL_CONTROLLER_AXIS_RIGHTY
) +
211 SDL_GameControllerGetAxis(sdlController
,SDL_CONTROLLER_AXIS_LEFTY
)) /
217 void SFG_processEvent(uint8_t event
, uint8_t data
)
221 int8_t SFG_keyPressed(uint8_t key
)
223 if (webKeyboardState
[key
]) // this only takes effect in the web version
226 #define k(x) sdlKeyboardState[SDL_SCANCODE_ ## x]
227 #define b(x) ((sdlController != NULL) && \
228 SDL_GameControllerGetButton(sdlController,SDL_CONTROLLER_BUTTON_ ## x))
232 case SFG_KEY_UP
: return k(UP
) || k(W
) || k(KP_8
) || b(DPAD_UP
); break;
234 return k(RIGHT
) || k(E
) || k(KP_6
) || b(DPAD_RIGHT
); break;
236 return k(DOWN
) || k(S
) || k(KP_5
) || k(KP_2
) || b(DPAD_DOWN
); break;
237 case SFG_KEY_LEFT
: return k(LEFT
) || k(Q
) || k(KP_4
) || b(DPAD_LEFT
); break;
238 case SFG_KEY_A
: return k(J
) || k(RETURN
) || k(LCTRL
) || k(RCTRL
) || b(X
) ||
239 b(RIGHTSTICK
) || (sdlMouseButtonState
& SDL_BUTTON_LMASK
); break;
240 case SFG_KEY_B
: return k(K
) || k(LSHIFT
) || b(B
); break;
241 case SFG_KEY_C
: return k(L
) || b(Y
); break;
242 case SFG_KEY_JUMP
: return k(SPACE
) || b(A
); break;
243 case SFG_KEY_STRAFE_LEFT
: return k(A
) || k(KP_7
); break;
244 case SFG_KEY_STRAFE_RIGHT
: return k(D
) || k(KP_9
); break;
245 case SFG_KEY_MAP
: return k(TAB
) || b(BACK
); break;
246 case SFG_KEY_CYCLE_WEAPON
: return k(F
) ||
247 (sdlMouseButtonState
& SDL_BUTTON_MMASK
); break;
248 case SFG_KEY_TOGGLE_FREELOOK
: return b(LEFTSTICK
) ||
249 (sdlMouseButtonState
& SDL_BUTTON_RMASK
); break;
250 case SFG_KEY_MENU
: return k(ESCAPE
) || b(START
); break;
251 case SFG_KEY_NEXT_WEAPON
:
252 if (k(P
) || k(X
) || b(RIGHTSHOULDER
))
255 #define checkMouse(cmp)\
256 if (sdlMouseWheelState cmp 0) { sdlMouseWheelState = 0; return 1; }
263 case SFG_KEY_PREVIOUS_WEAPON
:
264 if (k(O
) || k(Y
) || k(Z
) || b(LEFTSHOULDER
))
274 default: return 0; break;
283 void mainLoopIteration()
287 #ifdef __EMSCRIPTEN__
288 // hack, without it sound won't work because of shitty browser audio policies
290 if (SFG_game
.frame
% 512 == 0)
294 while (SDL_PollEvent(&event
)) // also automatically updates sdlKeyboardState
296 if (event
.type
== SDL_MOUSEWHEEL
)
298 if (event
.wheel
.y
> 0) // scroll up
299 sdlMouseWheelState
= 1;
300 else if (event
.wheel
.y
< 0) // scroll down
301 sdlMouseWheelState
= -1;
303 else if (event
.type
== SDL_QUIT
)
305 else if (event
.type
== SDL_MOUSEMOTION
)
309 sdlMouseButtonState
= SDL_GetMouseState(NULL
,NULL
);
311 if (!SFG_mainLoopBody())
314 SDL_UpdateTexture(texture
,NULL
,sdlScreen
,
315 SFG_SCREEN_RESOLUTION_X
* sizeof(uint16_t));
317 SDL_RenderClear(renderer
);
318 SDL_RenderCopy(renderer
,texture
,NULL
,NULL
);
319 SDL_RenderPresent(renderer
);
322 #ifdef __EMSCRIPTEN__
323 typedef void (*em_callback_func
)(void);
324 void emscripten_set_main_loop(
325 em_callback_func func
, int fps
, int simulate_infinite_loop
);
328 uint16_t audioBuff
[SFG_SFX_SAMPLE_COUNT
];
329 uint16_t audioPos
= 0; // audio position for the next audio buffer fill
330 uint32_t audioUpdateFrame
= 0; // game frame at which audio buffer fill happened
332 static inline int16_t mixSamples(int16_t sample1
, int16_t sample2
)
334 return sample1
+ sample2
;
338 // ^ this has to be init to 0 (not 1), else a few samples get played at start
340 void audioFillCallback(void *userdata
, uint8_t *s
, int l
)
342 uint16_t *s16
= (uint16_t *) s
;
344 for (int i
= 0; i
< l
/ 2; ++i
)
347 mixSamples(audioBuff
[audioPos
], SDL_MUSIC_VOLUME
*
348 (SFG_getNextMusicSample() - SFG_musicTrackAverages
[SFG_MusicState
.track
]))
349 : audioBuff
[audioPos
];
351 audioBuff
[audioPos
] = 0;
352 audioPos
= (audioPos
< SFG_SFX_SAMPLE_COUNT
- 1) ? (audioPos
+ 1) : 0;
355 audioUpdateFrame
= SFG_game
.frame
;
358 void SFG_setMusic(uint8_t value
)
362 case SFG_MUSIC_TURN_ON
: musicOn
= 1; break;
363 case SFG_MUSIC_TURN_OFF
: musicOn
= 0; break;
364 case SFG_MUSIC_NEXT
: SFG_nextMusicTrack(); break;
369 void SFG_playSound(uint8_t soundIndex
, uint8_t volume
)
371 uint16_t pos
= (audioPos
+
372 ((SFG_game
.frame
- audioUpdateFrame
) * SFG_MS_PER_FRAME
* 8)) %
373 SFG_SFX_SAMPLE_COUNT
;
375 uint16_t volumeScale
= 1 << (volume
/ 37);
377 for (int i
= 0; i
< SFG_SFX_SAMPLE_COUNT
; ++i
)
379 audioBuff
[pos
] = mixSamples(audioBuff
[pos
],
380 (128 - SFG_GET_SFX_SAMPLE(soundIndex
,i
)) * volumeScale
);
382 pos
= (pos
< SFG_SFX_SAMPLE_COUNT
- 1) ? (pos
+ 1) : 0;
386 void handleSignal(int signal
)
391 int main(int argc
, char *argv
[])
394 uint8_t argForceWindow
= 0;
395 uint8_t argForceFullscreen
= 0;
397 #ifndef __EMSCRIPTEN__
398 argForceFullscreen
= 1;
401 for (uint8_t i
= 0; i
< SFG_KEY_COUNT
; ++i
)
402 webKeyboardState
[i
] = 0;
404 for (uint8_t i
= 1; i
< argc
; ++i
)
406 if (argv
[i
][0] == '-' && argv
[i
][1] == 'h' && argv
[i
][2] == 0)
408 else if (argv
[i
][0] == '-' && argv
[i
][1] == 'w' && argv
[i
][2] == 0)
410 else if (argv
[i
][0] == '-' && argv
[i
][1] == 'f' && argv
[i
][2] == 0)
411 argForceFullscreen
= 1;
413 puts("SDL: unknown argument");
418 puts("Anarch (SDL), version " SFG_VERSION_STRING
"\n");
419 puts("Anarch is a unique suckless FPS game. Collect weapons and items and destroy");
420 puts("robot enemies in your way in order to get to the level finish. Some door are");
421 puts("locked and require access cards. Good luck!\n");
422 puts("created by Miloslav \"drummyfish\" Ciz, 2020, released under CC0 1.0 (public domain)\n");
423 puts("CLI flags:\n");
424 puts("-h print this help and exit");
425 puts("-w force window");
426 puts("-f force fullscreen\n");
428 puts("- arrows, numpad, [W] [S] [A] [D] [Q] [E]: movement");
429 puts("- mouse: rotation, [LMB] shoot, [RMB] toggle free look");
430 puts("- [SPACE]: jump");
431 puts("- [J] [RETURN] [CTRL] [LMB]: game A button (shoot, confirm)");
432 puts("- [K] [SHIFT]: game B button (cancel, strafe)");
433 puts("- [L]: game C button (+ down = menu, + up = jump, ...)");
434 puts("- [F]: cycle next/previous weapon");
435 puts("- [O] [P] [X] [Y] [Z] [mouse wheel] [mouse middle]: change weapons");
436 puts("- [TAB]: map");
437 puts("- [ESCAPE]: menu");
444 puts("SDL: initializing SDL");
446 SDL_Init(SDL_INIT_AUDIO
| SDL_INIT_JOYSTICK
);
449 SDL_CreateWindow("Anarch", SDL_WINDOWPOS_UNDEFINED
,
450 SDL_WINDOWPOS_UNDEFINED
, SFG_SCREEN_RESOLUTION_X
, SFG_SCREEN_RESOLUTION_Y
,
453 renderer
= SDL_CreateRenderer(window
,-1,0);
456 SDL_CreateTexture(renderer
,SDL_PIXELFORMAT_RGB565
,SDL_TEXTUREACCESS_STATIC
,
457 SFG_SCREEN_RESOLUTION_X
,SFG_SCREEN_RESOLUTION_Y
);
459 screenSurface
= SDL_GetWindowSurface(window
);
462 argForceFullscreen
= 1;
465 if (!argForceWindow
&& argForceFullscreen
)
467 puts("SDL: setting fullscreen");
468 SDL_SetWindowFullscreen(window
,SDL_WINDOW_FULLSCREEN_DESKTOP
);
471 sdlKeyboardState
= SDL_GetKeyboardState(NULL
);
473 sdlController
= SDL_GameControllerOpen(0);
475 #if !SFG_OS_IS_MALWARE
476 signal(SIGINT
,handleSignal
);
477 signal(SIGQUIT
,handleSignal
);
478 signal(SIGTERM
,handleSignal
);
481 SDL_AudioSpec audioSpec
;
483 SDL_memset(&audioSpec
, 0, sizeof(audioSpec
));
484 audioSpec
.callback
= audioFillCallback
;
485 audioSpec
.freq
= 8000;
486 audioSpec
.format
= AUDIO_S16
;
487 audioSpec
.channels
= 1;
488 #ifdef __EMSCRIPTEN__
489 audioSpec
.samples
= 1024;
491 audioSpec
.samples
= 256;
494 if (SDL_OpenAudio(&audioSpec
,NULL
) < 0)
495 puts("SDL: could not initialize audio");
497 for (int16_t i
= 0; i
< SFG_SFX_SAMPLE_COUNT
; ++i
)
507 SDL_GameControllerUpdate();
509 SDL_WarpMouseInWindow(window
,
510 SFG_SCREEN_RESOLUTION_X
/ 2, SFG_SCREEN_RESOLUTION_Y
/ 2);
512 #ifdef __EMSCRIPTEN__
513 emscripten_set_main_loop(mainLoopIteration
,0,1);
519 puts("SDL: freeing SDL");
521 SDL_GameControllerClose(sdlController
);
524 SDL_DestroyTexture(texture
);
525 SDL_DestroyRenderer(renderer
);
526 SDL_DestroyWindow(window
);