├── .gitignore ├── circle.yml ├── data ├── DejaVuSans.ttf ├── default.map ├── grass.png └── player.png ├── glibc-hack └── nimbase.h ├── libs ├── win32 │ ├── SDL2.dll │ ├── SDL2_image.dll │ ├── SDL2_ttf.dll │ ├── libfreetype-6.dll │ ├── libpng16-16.dll │ └── zlib1.dll └── win64 │ ├── SDL2.dll │ ├── SDL2_image.dll │ ├── SDL2_ttf.dll │ ├── libfreetype-6.dll │ ├── libpng16-16.dll │ └── zlib1.dll ├── license.txt ├── platformer.nim ├── platformer.nimble ├── readme.md ├── release.nim └── tutorial ├── DejaVuSans.ttf ├── default.map ├── grass.png ├── platformer_part1.nim ├── platformer_part2.nim ├── platformer_part3.nim ├── platformer_part4.nim ├── platformer_part5.nim ├── platformer_part6.nim ├── platformer_part7.nim ├── platformer_part8.nim ├── platformer_part9.nim └── player.png /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*/ 3 | !*.* 4 | nimcache/ 5 | *.swp 6 | *.exe 7 | builds/ 8 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | pre: 3 | - | 4 | if [ ! -x ~/nim/bin/nim ]; then 5 | git clone -b devel --depth 1 git://github.com/nim-lang/nim ~/nim/ 6 | git clone --depth 1 git://github.com/nim-lang/csources ~/nim/csources/ 7 | cd ~/nim/csources; sh build.sh; cd ..; rm -rf csources 8 | bin/nim c koch; ./koch boot -d:release 9 | ln -fs ~/nim/bin/nim ~/bin/nim 10 | else 11 | cd ~/nim; git fetch origin 12 | git merge FETCH_HEAD | grep "Already up-to-date" || (bin/nim c koch; ./koch boot -d:release) 13 | fi 14 | if [ ! -x ~/nimble ]; then 15 | git clone --depth 1 https://github.com/nim-lang/nimble ~/nimble/ 16 | cd ~/nimble 17 | nim -d:release c -r src/nimble -y install 18 | ln -fs ~/.nimble/bin/nimble ~/bin/nimble 19 | else 20 | cd ~/nimble; git fetch origin 21 | git merge FETCH_HEAD | grep "Already up-to-date" || (nim -d:release c -r src/nimble -y install) 22 | fi 23 | nimble update 24 | 25 | cache_directories: 26 | - "~/bin/" 27 | - "~/nim/" 28 | - "~/nimble/" 29 | - "~/.nimble/" 30 | 31 | ## Customize test commands 32 | test: 33 | override: 34 | - | 35 | nimble install 36 | nimble tests 37 | -------------------------------------------------------------------------------- /data/DejaVuSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/data/DejaVuSans.ttf -------------------------------------------------------------------------------- /data/default.map: -------------------------------------------------------------------------------- 1 | 0 0 0 0 78 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 0 0 2 | 4 5 0 0 78 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 4 5 3 | 20 21 0 0 78 0 0 0 0 0 0 0 0 0 0 0 0 0 4 5 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 20 21 4 | 20 21 0 0 78 0 0 0 0 0 0 0 0 4 5 0 0 0 36 37 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 20 21 5 | 20 21 0 0 78 0 0 0 0 0 0 0 0 20 21 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 20 21 6 | 20 21 0 0 78 0 0 0 0 0 0 0 0 20 21 0 0 0 0 0 0 0 4 16 16 16 16 5 0 0 0 0 110 0 0 20 21 7 | 20 21 0 0 78 0 0 4 5 0 0 0 0 20 21 0 0 0 0 0 0 0 36 52 54 53 52 37 0 0 0 0 110 0 0 20 21 8 | 20 21 0 0 78 0 0 20 21 0 0 0 0 20 21 0 0 0 0 0 0 0 0 0 20 21 0 0 0 0 0 0 110 0 0 20 21 9 | 20 38 0 0 78 0 0 22 38 0 0 0 0 22 38 0 0 0 0 0 0 0 0 0 22 38 0 0 0 0 0 0 110 0 0 22 21 10 | 20 49 16 16 16 16 16 48 49 16 16 16 16 48 49 16 16 16 16 16 16 16 16 16 48 49 16 16 16 16 16 16 16 16 16 48 21 11 | 36 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 37 12 | -------------------------------------------------------------------------------- /data/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/data/grass.png -------------------------------------------------------------------------------- /data/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/data/player.png -------------------------------------------------------------------------------- /glibc-hack/nimbase.h: -------------------------------------------------------------------------------- 1 | __asm__(".symver memcpy,memcpy@GLIBC_2.2.5"); 2 | __asm__(".symver realpath,realpath@GLIBC_2.2.5"); 3 | /* 4 | 5 | Nim's Runtime Library 6 | (c) Copyright 2015 Andreas Rumpf 7 | 8 | See the file "copying.txt", included in this 9 | distribution, for details about the copyright. 10 | */ 11 | 12 | /* compiler symbols: 13 | __BORLANDC__ 14 | _MSC_VER 15 | __WATCOMC__ 16 | __LCC__ 17 | __GNUC__ 18 | __DMC__ 19 | __POCC__ 20 | __TINYC__ 21 | __clang__ 22 | */ 23 | 24 | #ifndef NIMBASE_H 25 | #define NIMBASE_H 26 | 27 | /* ------------ ignore typical warnings in Nim-generated files ------------- */ 28 | #if defined(__GNUC__) || defined(__clang__) 29 | # pragma GCC diagnostic ignored "-Wpragmas" 30 | # pragma GCC diagnostic ignored "-Wwritable-strings" 31 | # pragma GCC diagnostic ignored "-Winvalid-noreturn" 32 | # pragma GCC diagnostic ignored "-Wformat" 33 | # pragma GCC diagnostic ignored "-Wlogical-not-parentheses" 34 | # pragma GCC diagnostic ignored "-Wlogical-op-parentheses" 35 | # pragma GCC diagnostic ignored "-Wshadow" 36 | # pragma GCC diagnostic ignored "-Wunused-function" 37 | # pragma GCC diagnostic ignored "-Wunused-variable" 38 | # pragma GCC diagnostic ignored "-Winvalid-offsetof" 39 | # pragma GCC diagnostic ignored "-Wtautological-compare" 40 | # pragma GCC diagnostic ignored "-Wswitch-bool" 41 | # pragma GCC diagnostic ignored "-Wmacro-redefined" 42 | # pragma GCC diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers" 43 | #endif 44 | 45 | #if defined(_MSC_VER) 46 | # pragma warning(disable: 4005 4100 4101 4189 4191 4200 4244 4293 4296 4309) 47 | # pragma warning(disable: 4310 4365 4456 4477 4514 4574 4611 4668 4702 4706) 48 | # pragma warning(disable: 4710 4711 4774 4800 4820 4996) 49 | #endif 50 | /* ------------------------------------------------------------------------- */ 51 | 52 | #if defined(__GNUC__) 53 | # define _GNU_SOURCE 1 54 | #endif 55 | 56 | #if defined(__TINYC__) 57 | /*# define __GNUC__ 3 58 | # define GCC_MAJOR 4 59 | # define __GNUC_MINOR__ 4 60 | # define __GNUC_PATCHLEVEL__ 5 */ 61 | # define __DECLSPEC_SUPPORTED 1 62 | #endif 63 | 64 | /* calling convention mess ----------------------------------------------- */ 65 | #if defined(__GNUC__) || defined(__LCC__) || defined(__POCC__) \ 66 | || defined(__TINYC__) 67 | /* these should support C99's inline */ 68 | /* the test for __POCC__ has to come before the test for _MSC_VER, 69 | because PellesC defines _MSC_VER too. This is brain-dead. */ 70 | # define N_INLINE(rettype, name) inline rettype name 71 | #elif defined(__BORLANDC__) || defined(_MSC_VER) 72 | /* Borland's compiler is really STRANGE here; note that the __fastcall 73 | keyword cannot be before the return type, but __inline cannot be after 74 | the return type, so we do not handle this mess in the code generator 75 | but rather here. */ 76 | # define N_INLINE(rettype, name) __inline rettype name 77 | #elif defined(__DMC__) 78 | # define N_INLINE(rettype, name) inline rettype name 79 | #elif defined(__WATCOMC__) 80 | # define N_INLINE(rettype, name) __inline rettype name 81 | #else /* others are less picky: */ 82 | # define N_INLINE(rettype, name) rettype __inline name 83 | #endif 84 | 85 | #if defined(__POCC__) 86 | # define NIM_CONST /* PCC is really picky with const modifiers */ 87 | # undef _MSC_VER /* Yeah, right PCC defines _MSC_VER even if it is 88 | not that compatible. Well done. */ 89 | #elif defined(__cplusplus) 90 | # define NIM_CONST /* C++ is picky with const modifiers */ 91 | #else 92 | # define NIM_CONST const 93 | #endif 94 | 95 | /* 96 | NIM_THREADVAR declaration based on 97 | http://stackoverflow.com/questions/18298280/how-to-declare-a-variable-as-thread-local-portably 98 | */ 99 | #if __STDC_VERSION__ >= 201112 && !defined __STDC_NO_THREADS__ 100 | # define NIM_THREADVAR _Thread_local 101 | #elif defined _WIN32 && ( \ 102 | defined _MSC_VER || \ 103 | defined __ICL || \ 104 | defined __DMC__ || \ 105 | defined __BORLANDC__ ) 106 | # define NIM_THREADVAR __declspec(thread) 107 | /* note that ICC (linux) and Clang are covered by __GNUC__ */ 108 | #elif defined __GNUC__ || \ 109 | defined __SUNPRO_C || \ 110 | defined __xlC__ 111 | # define NIM_THREADVAR __thread 112 | #elif defined __TINYC__ 113 | # define NIM_THREADVAR 114 | #else 115 | # error "Cannot define NIM_THREADVAR" 116 | #endif 117 | 118 | /* --------------- how int64 constants should be declared: ----------- */ 119 | #if defined(__GNUC__) || defined(__LCC__) || \ 120 | defined(__POCC__) || defined(__DMC__) || defined(_MSC_VER) 121 | # define IL64(x) x##LL 122 | #else /* works only without LL */ 123 | # define IL64(x) ((NI64)x) 124 | #endif 125 | 126 | /* ---------------- casting without correct aliasing rules ----------- */ 127 | 128 | #if defined(__GNUC__) 129 | # define NIM_CAST(type, ptr) (((union{type __x__;}*)(ptr))->__x__) 130 | #else 131 | # define NIM_CAST(type, ptr) ((type)(ptr)) 132 | #endif 133 | 134 | /* ------------------------------------------------------------------- */ 135 | 136 | #if defined(WIN32) || defined(_WIN32) /* only Windows has this mess... */ 137 | # define N_CDECL(rettype, name) rettype __cdecl name 138 | # define N_STDCALL(rettype, name) rettype __stdcall name 139 | # define N_SYSCALL(rettype, name) rettype __syscall name 140 | # define N_FASTCALL(rettype, name) rettype __fastcall name 141 | # define N_SAFECALL(rettype, name) rettype __safecall name 142 | /* function pointers with calling convention: */ 143 | # define N_CDECL_PTR(rettype, name) rettype (__cdecl *name) 144 | # define N_STDCALL_PTR(rettype, name) rettype (__stdcall *name) 145 | # define N_SYSCALL_PTR(rettype, name) rettype (__syscall *name) 146 | # define N_FASTCALL_PTR(rettype, name) rettype (__fastcall *name) 147 | # define N_SAFECALL_PTR(rettype, name) rettype (__safecall *name) 148 | 149 | # ifdef __cplusplus 150 | # define N_LIB_EXPORT extern "C" __declspec(dllexport) 151 | # else 152 | # define N_LIB_EXPORT extern __declspec(dllexport) 153 | # endif 154 | # define N_LIB_IMPORT extern __declspec(dllimport) 155 | #else 156 | # if defined(__GNUC__) 157 | # define N_CDECL(rettype, name) rettype name 158 | # define N_STDCALL(rettype, name) rettype name 159 | # define N_SYSCALL(rettype, name) rettype name 160 | # define N_FASTCALL(rettype, name) __attribute__((fastcall)) rettype name 161 | # define N_SAFECALL(rettype, name) rettype name 162 | /* function pointers with calling convention: */ 163 | # define N_CDECL_PTR(rettype, name) rettype (*name) 164 | # define N_STDCALL_PTR(rettype, name) rettype (*name) 165 | # define N_SYSCALL_PTR(rettype, name) rettype (*name) 166 | # define N_FASTCALL_PTR(rettype, name) __attribute__((fastcall)) rettype (*name) 167 | # define N_SAFECALL_PTR(rettype, name) rettype (*name) 168 | # else 169 | # define N_CDECL(rettype, name) rettype name 170 | # define N_STDCALL(rettype, name) rettype name 171 | # define N_SYSCALL(rettype, name) rettype name 172 | # define N_FASTCALL(rettype, name) rettype name 173 | # define N_SAFECALL(rettype, name) rettype name 174 | /* function pointers with calling convention: */ 175 | # define N_CDECL_PTR(rettype, name) rettype (*name) 176 | # define N_STDCALL_PTR(rettype, name) rettype (*name) 177 | # define N_SYSCALL_PTR(rettype, name) rettype (*name) 178 | # define N_FASTCALL_PTR(rettype, name) rettype (*name) 179 | # define N_SAFECALL_PTR(rettype, name) rettype (*name) 180 | # endif 181 | # ifdef __cplusplus 182 | # define N_LIB_EXPORT extern "C" 183 | # else 184 | # define N_LIB_EXPORT extern 185 | # endif 186 | # define N_LIB_IMPORT extern 187 | #endif 188 | 189 | #define N_NOCONV(rettype, name) rettype name 190 | /* specify no calling convention */ 191 | #define N_NOCONV_PTR(rettype, name) rettype (*name) 192 | 193 | #if defined(__GNUC__) || defined(__ICC__) 194 | # define N_NOINLINE(rettype, name) rettype __attribute__((noinline)) name 195 | #elif defined(_MSC_VER) 196 | # define N_NOINLINE(rettype, name) __declspec(noinline) rettype name 197 | #else 198 | # define N_NOINLINE(rettype, name) rettype name 199 | #endif 200 | 201 | #define N_NOINLINE_PTR(rettype, name) rettype (*name) 202 | 203 | #if defined(__BORLANDC__) || defined(__WATCOMC__) || \ 204 | defined(__POCC__) || defined(_MSC_VER) || defined(WIN32) || defined(_WIN32) 205 | /* these compilers have a fastcall so use it: */ 206 | # ifdef __TINYC__ 207 | # define N_NIMCALL(rettype, name) rettype __attribute((__fastcall)) name 208 | # define N_NIMCALL_PTR(rettype, name) rettype (__attribute((__fastcall)) *name) 209 | # define N_RAW_NIMCALL __attribute((__fastcall)) 210 | # else 211 | # define N_NIMCALL(rettype, name) rettype __fastcall name 212 | # define N_NIMCALL_PTR(rettype, name) rettype (__fastcall *name) 213 | # define N_RAW_NIMCALL __fastcall 214 | # endif 215 | #else 216 | # define N_NIMCALL(rettype, name) rettype name /* no modifier */ 217 | # define N_NIMCALL_PTR(rettype, name) rettype (*name) 218 | # define N_RAW_NIMCALL 219 | #endif 220 | 221 | #define N_CLOSURE(rettype, name) N_NIMCALL(rettype, name) 222 | #define N_CLOSURE_PTR(rettype, name) N_NIMCALL_PTR(rettype, name) 223 | 224 | /* ----------------------------------------------------------------------- */ 225 | 226 | #define COMMA , 227 | 228 | #include 229 | #include 230 | 231 | /* C99 compiler? */ 232 | #if (defined(__STD_VERSION__) && (__STD_VERSION__ >= 199901)) 233 | # define HAVE_STDINT_H 234 | #endif 235 | 236 | #if defined(__LCC__) || defined(__DMC__) || defined(__POCC__) 237 | # define HAVE_STDINT_H 238 | #endif 239 | 240 | /* bool types (C++ has it): */ 241 | #ifdef __cplusplus 242 | # ifndef NIM_TRUE 243 | # define NIM_TRUE true 244 | # endif 245 | # ifndef NIM_FALSE 246 | # define NIM_FALSE false 247 | # endif 248 | # define NIM_BOOL bool 249 | # define NIM_NIL 0 250 | struct NimException 251 | { 252 | NimException(struct Exception* exp, const char* msg): exp(exp), msg(msg) {} 253 | 254 | struct Exception* exp; 255 | const char* msg; 256 | }; 257 | #else 258 | # ifdef bool 259 | # define NIM_BOOL bool 260 | # else 261 | typedef unsigned char NIM_BOOL; 262 | # endif 263 | # ifndef NIM_TRUE 264 | # define NIM_TRUE ((NIM_BOOL) 1) 265 | # endif 266 | # ifndef NIM_FALSE 267 | # define NIM_FALSE ((NIM_BOOL) 0) 268 | # endif 269 | # define NIM_NIL ((void*)0) /* C's NULL is fucked up in some C compilers, so 270 | the generated code does not rely on it anymore */ 271 | #endif 272 | 273 | #if defined(__BORLANDC__) || defined(__DMC__) \ 274 | || defined(__WATCOMC__) || defined(_MSC_VER) 275 | typedef signed char NI8; 276 | typedef signed short int NI16; 277 | typedef signed int NI32; 278 | /* XXX: Float128? */ 279 | typedef unsigned char NU8; 280 | typedef unsigned short int NU16; 281 | typedef unsigned __int64 NU64; 282 | typedef __int64 NI64; 283 | typedef unsigned int NU32; 284 | #elif defined(HAVE_STDINT_H) 285 | # include 286 | typedef int8_t NI8; 287 | typedef int16_t NI16; 288 | typedef int32_t NI32; 289 | typedef int64_t NI64; 290 | typedef uint64_t NU64; 291 | typedef uint8_t NU8; 292 | typedef uint16_t NU16; 293 | typedef uint32_t NU32; 294 | #else 295 | typedef signed char NI8; 296 | typedef signed short int NI16; 297 | typedef signed int NI32; 298 | /* XXX: Float128? */ 299 | typedef unsigned char NU8; 300 | typedef unsigned short int NU16; 301 | typedef unsigned long long int NU64; 302 | typedef long long int NI64; 303 | typedef unsigned int NU32; 304 | #endif 305 | 306 | #ifdef NIM_INTBITS 307 | # if NIM_INTBITS == 64 308 | typedef NI64 NI; 309 | typedef NU64 NU; 310 | # elif NIM_INTBITS == 32 311 | typedef NI32 NI; 312 | typedef NU32 NU; 313 | # elif NIM_INTBITS == 16 314 | typedef NI16 NI; 315 | typedef NU16 NU; 316 | # elif NIM_INTBITS == 8 317 | typedef NI8 NI; 318 | typedef NU8 NU; 319 | # else 320 | # error "invalid bit width for int" 321 | # endif 322 | #endif 323 | 324 | extern NI nim_program_result; 325 | 326 | typedef float NF32; 327 | typedef double NF64; 328 | typedef double NF; 329 | 330 | typedef char NIM_CHAR; 331 | typedef char* NCSTRING; 332 | 333 | #ifdef NIM_BIG_ENDIAN 334 | # define NIM_IMAN 1 335 | #else 336 | # define NIM_IMAN 0 337 | #endif 338 | 339 | static N_INLINE(NI, float64ToInt32)(double x) { 340 | /* nowadays no hack necessary anymore */ 341 | return x >= 0 ? (NI)(x+0.5) : (NI)(x-0.5); 342 | } 343 | 344 | static N_INLINE(NI32, float32ToInt32)(float x) { 345 | /* nowadays no hack necessary anymore */ 346 | return x >= 0 ? (NI32)(x+0.5) : (NI32)(x-0.5); 347 | } 348 | 349 | #define float64ToInt64(x) ((NI64) (x)) 350 | 351 | #define zeroMem(a, size) memset(a, 0, size) 352 | #define equalMem(a, b, size) (memcmp(a, b, size) == 0) 353 | 354 | #define STRING_LITERAL(name, str, length) \ 355 | static const struct { \ 356 | TGenericSeq Sup; \ 357 | NIM_CHAR data[(length) + 1]; \ 358 | } name = {{length, length}, str} 359 | 360 | typedef struct TStringDesc* string; 361 | 362 | /* declared size of a sequence/variable length array: */ 363 | #if defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER) 364 | # define SEQ_DECL_SIZE /* empty is correct! */ 365 | #else 366 | # define SEQ_DECL_SIZE 1000000 367 | #endif 368 | 369 | #define ALLOC_0(size) calloc(1, size) 370 | #define DL_ALLOC_0(size) dlcalloc(1, size) 371 | 372 | #define GenericSeqSize sizeof(TGenericSeq) 373 | #define paramCount() cmdCount 374 | 375 | #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__i386__) 376 | # ifndef NAN 377 | static unsigned long nimNaN[2]={0xffffffff, 0x7fffffff}; 378 | # define NAN (*(double*) nimNaN) 379 | # endif 380 | #endif 381 | 382 | #ifndef NAN 383 | # define NAN (0.0 / 0.0) 384 | #endif 385 | 386 | #ifndef INF 387 | # ifdef INFINITY 388 | # define INF INFINITY 389 | # elif defined(HUGE_VAL) 390 | # define INF HUGE_VAL 391 | # elif defined(_MSC_VER) 392 | # include 393 | # define INF (DBL_MAX+DBL_MAX) 394 | # else 395 | # define INF (1.0 / 0.0) 396 | # endif 397 | #endif 398 | 399 | typedef struct TFrame TFrame; 400 | struct TFrame { 401 | TFrame* prev; 402 | NCSTRING procname; 403 | NI line; 404 | NCSTRING filename; 405 | NI16 len; 406 | NI16 calldepth; 407 | }; 408 | 409 | #define nimfr(proc, file) \ 410 | TFrame FR; \ 411 | FR.procname = proc; FR.filename = file; FR.line = 0; FR.len = 0; nimFrame(&FR); 412 | 413 | #define nimfrs(proc, file, slots, length) \ 414 | struct {TFrame* prev;NCSTRING procname;NI line;NCSTRING filename; NI len; VarSlot s[slots];} FR; \ 415 | FR.procname = proc; FR.filename = file; FR.line = 0; FR.len = length; nimFrame((TFrame*)&FR); 416 | 417 | #define nimln(n, file) \ 418 | FR.line = n; FR.filename = file; 419 | 420 | #define NIM_POSIX_INIT __attribute__((constructor)) 421 | 422 | #if defined(_MSCVER) && defined(__i386__) 423 | __declspec(naked) int __fastcall NimXadd(volatile int* pNum, int val) { 424 | __asm { 425 | lock xadd dword ptr [ECX], EDX 426 | mov EAX, EDX 427 | ret 428 | } 429 | } 430 | #endif 431 | 432 | #ifdef __GNUC__ 433 | # define likely(x) __builtin_expect(x, 1) 434 | # define unlikely(x) __builtin_expect(x, 0) 435 | /* We need the following for the posix wrapper. In particular it will give us 436 | POSIX_SPAWN_USEVFORK: */ 437 | # ifndef _GNU_SOURCE 438 | # define _GNU_SOURCE 439 | # endif 440 | #else 441 | # define likely(x) (x) 442 | # define unlikely(x) (x) 443 | #endif 444 | 445 | #if 0 // defined(__GNUC__) || defined(__clang__) 446 | // not needed anymore because the stack marking cares about 447 | // interior pointers now 448 | static inline void GCGuard (void *ptr) { asm volatile ("" :: "X" (ptr)); } 449 | # define GC_GUARD __attribute__ ((cleanup(GCGuard))) 450 | #else 451 | # define GC_GUARD 452 | #endif 453 | 454 | /* Test to see if Nim and the C compiler agree on the size of a pointer. 455 | On disagreement, your C compiler will say something like: 456 | "error: 'assert_numbits' declared as an array with a negative size" */ 457 | typedef int assert_numbits[sizeof(NI) == sizeof(void*) && NIM_INTBITS == sizeof(NI)*8 ? 1 : -1]; 458 | #endif 459 | 460 | #ifdef __cplusplus 461 | # define NIM_EXTERNC extern "C" 462 | #else 463 | # define NIM_EXTERNC 464 | #endif 465 | 466 | /* ---------------- platform specific includes ----------------------- */ 467 | 468 | /* VxWorks related includes */ 469 | #if defined(__VXWORKS__) 470 | # include 471 | # include 472 | # include 473 | #elif defined(__FreeBSD__) 474 | # include 475 | #endif 476 | -------------------------------------------------------------------------------- /libs/win32/SDL2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win32/SDL2.dll -------------------------------------------------------------------------------- /libs/win32/SDL2_image.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win32/SDL2_image.dll -------------------------------------------------------------------------------- /libs/win32/SDL2_ttf.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win32/SDL2_ttf.dll -------------------------------------------------------------------------------- /libs/win32/libfreetype-6.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win32/libfreetype-6.dll -------------------------------------------------------------------------------- /libs/win32/libpng16-16.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win32/libpng16-16.dll -------------------------------------------------------------------------------- /libs/win32/zlib1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win32/zlib1.dll -------------------------------------------------------------------------------- /libs/win64/SDL2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win64/SDL2.dll -------------------------------------------------------------------------------- /libs/win64/SDL2_image.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win64/SDL2_image.dll -------------------------------------------------------------------------------- /libs/win64/SDL2_ttf.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win64/SDL2_ttf.dll -------------------------------------------------------------------------------- /libs/win64/libfreetype-6.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win64/libfreetype-6.dll -------------------------------------------------------------------------------- /libs/win64/libpng16-16.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win64/libpng16-16.dll -------------------------------------------------------------------------------- /libs/win64/zlib1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/libs/win64/zlib1.dll -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 Dennis Felsing. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | [ MIT license: http://www.opensource.org/licenses/mit-license.php ] 22 | 23 | ------------------------------------------------------------------------ 24 | 25 | All graphics under 'data' except the font (which has its own license) are 26 | released under CC-BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/) 27 | and taken directly from Teeworlds (https://github.com/teeworlds/teeworlds). 28 | -------------------------------------------------------------------------------- /platformer.nim: -------------------------------------------------------------------------------- 1 | import 2 | sdl2, sdl2/image, sdl2/ttf, 3 | basic2d, strutils, times, math, strfmt, os, streams 4 | 5 | type 6 | SDLException = object of Exception 7 | 8 | Input {.pure.} = enum none, left, right, jump, restart, quit 9 | 10 | Collision {.pure.} = enum x, y, corner 11 | 12 | CacheLine = object 13 | texture: TexturePtr 14 | w, h: cint 15 | 16 | TextCache = ref object 17 | text: string 18 | cache: array[2, CacheLine] 19 | 20 | Time = ref object 21 | begin, finish, best: int 22 | 23 | Player = ref object 24 | texture: TexturePtr 25 | pos: Point2d 26 | vel: Vector2d 27 | time: Time 28 | 29 | Map = ref object 30 | texture: TexturePtr 31 | width, height: int 32 | tiles: seq[uint8] 33 | 34 | Game = ref object 35 | inputs: array[Input, bool] 36 | renderer: RendererPtr 37 | font: FontPtr 38 | player: Player 39 | map: Map 40 | camera: Vector2d 41 | 42 | const 43 | windowSize: Point = (1280.cint, 720.cint) 44 | 45 | tilesPerRow = 16 46 | tileSize: Point = (64.cint, 64.cint) 47 | 48 | playerSize = vector2d(64, 64) 49 | 50 | air = 0 51 | start = 78 52 | finish = 110 53 | 54 | template sdlFailIf(cond: typed, reason: string) = 55 | if cond: raise SDLException.newException( 56 | reason & ", SDL error: " & $getError()) 57 | 58 | proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) = 59 | let 60 | x = pos.x.cint 61 | y = pos.y.cint 62 | 63 | var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [ 64 | (rect(192, 64, 64, 32), rect(x-60, y, 96, 48), 65 | SDL_FLIP_NONE), # back feet shadow 66 | (rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96), 67 | SDL_FLIP_NONE), # body shadow 68 | (rect(192, 64, 64, 32), rect(x-36, y, 96, 48), 69 | SDL_FLIP_NONE), # front feet shadow 70 | (rect(192, 32, 64, 32), rect(x-60, y, 96, 48), 71 | SDL_FLIP_NONE), # back feet 72 | (rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96), 73 | SDL_FLIP_NONE), # body 74 | (rect(192, 32, 64, 32), rect(x-36, y, 96, 48), 75 | SDL_FLIP_NONE), # front feet 76 | (rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36), 77 | SDL_FLIP_NONE), # left eye 78 | (rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36), 79 | SDL_FLIP_HORIZONTAL) # right eye 80 | ] 81 | 82 | for part in bodyParts.mitems: 83 | renderer.copyEx(texture, part.source, part.dest, angle = 0.0, 84 | center = nil, flip = part.flip) 85 | 86 | proc renderMap(renderer: RendererPtr, map: Map, camera: Vector2d) = 87 | var 88 | clip = rect(0, 0, tileSize.x, tileSize.y) 89 | dest = rect(0, 0, tileSize.x, tileSize.y) 90 | 91 | for i, tileNr in map.tiles: 92 | if tileNr == 0: continue 93 | 94 | clip.x = cint(tileNr mod tilesPerRow) * tileSize.x 95 | clip.y = cint(tileNr div tilesPerRow) * tileSize.y 96 | dest.x = cint(i mod map.width) * tileSize.x - camera.x.cint 97 | dest.y = cint(i div map.width) * tileSize.y - camera.y.cint 98 | 99 | renderer.copy(map.texture, unsafeAddr clip, unsafeAddr dest) 100 | 101 | proc newTextCache: TextCache = 102 | new result 103 | 104 | proc renderText(renderer: RendererPtr, font: FontPtr, text: string, 105 | x, y, outline: cint, color: Color): CacheLine = 106 | font.setFontOutline(outline) 107 | let surface = font.renderUtf8Blended(text.cstring, color) 108 | sdlFailIf surface.isNil: "Could not render text surface" 109 | 110 | discard surface.setSurfaceAlphaMod(color.a) 111 | 112 | result.w = surface.w 113 | result.h = surface.h 114 | result.texture = renderer.createTextureFromSurface(surface) 115 | sdlFailIf result.texture.isNil: "Could not create texture from rendered text" 116 | 117 | surface.freeSurface() 118 | 119 | proc renderText(game: Game, text: string, x, y: cint, color: Color, 120 | tc: TextCache) = 121 | let passes = [(color: color(0, 0, 0, 64), outline: 2.cint), 122 | (color: color, outline: 0.cint)] 123 | 124 | if text != tc.text: 125 | for i in 0..1: 126 | tc.cache[i].texture.destroy() 127 | tc.cache[i] = game.renderer.renderText( 128 | game.font, text, x, y, passes[i].outline, passes[i].color) 129 | tc.text = text 130 | 131 | for i in 0..1: 132 | var source = rect(0, 0, tc.cache[i].w, tc.cache[i].h) 133 | var dest = rect(x - passes[i].outline, y - passes[i].outline, 134 | tc.cache[i].w, tc.cache[i].h) 135 | game.renderer.copyEx(tc.cache[i].texture, source, dest, 136 | angle = 0.0, center = nil) 137 | 138 | template renderTextCached(game: Game, text: string, x, y: cint, color: Color) = 139 | block: 140 | var tc {.global.} = newTextCache() 141 | game.renderText(text, x, y, color, tc) 142 | 143 | proc restartPlayer(player: Player) = 144 | player.pos = point2d(170, 500) 145 | player.vel = vector2d(0, 0) 146 | player.time.begin = -1 147 | player.time.finish = -1 148 | 149 | proc newTime: Time = 150 | new result 151 | result.finish = -1 152 | result.best = -1 153 | 154 | proc newPlayer(texture: TexturePtr): Player = 155 | new result 156 | result.texture = texture 157 | result.time = newTime() 158 | result.restartPlayer() 159 | 160 | proc newMap(texture: TexturePtr, map: Stream): Map = 161 | new result 162 | result.texture = texture 163 | result.tiles = @[] 164 | 165 | var line = "" 166 | while map.readLine(line): 167 | var width = 0 168 | for word in line.split(' '): 169 | if word == "": continue 170 | let value = parseUInt(word) 171 | if value > uint(uint8.high): 172 | raise ValueError.newException( 173 | "Invalid value in map: " & word) 174 | result.tiles.add value.uint8 175 | inc width 176 | 177 | if width == 0: continue 178 | if result.width > 0 and result.width != width: 179 | raise ValueError.newException( 180 | "Incompatible line length in map: " & $width) 181 | result.width = width 182 | inc result.height 183 | 184 | const dataDir = "data" 185 | 186 | when defined(embedData): 187 | template readRW(filename: string): ptr RWops = 188 | const file = staticRead(dataDir / filename) 189 | rwFromConstMem(file.cstring, file.len) 190 | 191 | template readStream(filename: string): Stream = 192 | const file = staticRead(dataDir / filename) 193 | newStringStream(file) 194 | else: 195 | let fullDataDir = getAppDir() / dataDir 196 | 197 | template readRW(filename: string): ptr RWops = 198 | var rw = rwFromFile(cstring(fullDataDir / filename), "r") 199 | sdlFailIf rw.isNil: "Cannot create RWops from file" 200 | rw 201 | 202 | template readStream(filename: string): Stream = 203 | var stream = newFileStream(fullDataDir / filename) 204 | if stream.isNil: raise ValueError.newException( 205 | "Cannot open file stream:" & fullDataDir / filename) 206 | stream 207 | 208 | proc newGame(renderer: RendererPtr): Game = 209 | new result 210 | result.renderer = renderer 211 | 212 | result.font = openFontRW( 213 | readRW("DejaVuSans.ttf"), freesrc = 1, 28) 214 | sdlFailIf result.font.isNil: "Failed to load font" 215 | 216 | result.player = newPlayer(renderer.loadTexture_RW( 217 | readRW("player.png"), freesrc = 1)) 218 | result.map = newMap(renderer.loadTexture_RW( 219 | readRW("grass.png"), freesrc = 1), 220 | readStream("default.map")) 221 | 222 | proc toInput(key: Scancode): Input = 223 | case key 224 | of SDL_SCANCODE_A: Input.left 225 | of SDL_SCANCODE_D: Input.right 226 | of SDL_SCANCODE_SPACE: Input.jump 227 | of SDL_SCANCODE_R: Input.restart 228 | of SDL_SCANCODE_Q: Input.quit 229 | else: Input.none 230 | 231 | proc handleInput(game: Game) = 232 | var event = defaultEvent 233 | while pollEvent(event): 234 | case event.kind 235 | of QuitEvent: 236 | game.inputs[Input.quit] = true 237 | of KeyDown: 238 | game.inputs[event.key.keysym.scancode.toInput] = true 239 | of KeyUp: 240 | game.inputs[event.key.keysym.scancode.toInput] = false 241 | else: 242 | discard 243 | 244 | proc formatTime(ticks: int): string = 245 | let mins = (ticks div 50) div 60 246 | let secs = (ticks div 50) mod 60 247 | interp"${mins:02}:${secs:02}" 248 | 249 | proc formatTimeExact(ticks: int): string = 250 | let cents = (ticks mod 50) * 2 251 | interp"${formatTime(ticks)}:${cents:02}" 252 | 253 | proc render(game: Game, tick: int) = 254 | # Draw over all drawings of the last frame with the default color 255 | game.renderer.clear() 256 | # Actual drawing here 257 | game.renderer.renderTee(game.player.texture, game.player.pos - game.camera) 258 | game.renderer.renderMap(game.map, game.camera) 259 | 260 | let time = game.player.time 261 | const white = color(255, 255, 255, 255) 262 | if time.begin >= 0: 263 | game.renderTextCached(formatTime(tick - time.begin), 50, 100, white) 264 | elif time.finish >= 0: 265 | game.renderTextCached("Finished in: " & formatTimeExact(time.finish), 266 | 50, 100, white) 267 | if time.best >= 0: 268 | game.renderTextCached("Best time: " & formatTimeExact(time.best), 269 | 50, 150, white) 270 | 271 | # Show the result on screen 272 | game.renderer.present() 273 | 274 | proc getTile(map: Map, x, y: int): uint8 = 275 | let 276 | nx = clamp(x div tileSize.x, 0, map.width - 1) 277 | ny = clamp(y div tileSize.y, 0, map.height - 1) 278 | pos = ny * map.width + nx 279 | 280 | map.tiles[pos] 281 | 282 | proc getTile(map: Map, pos: Point2d): uint8 = 283 | map.getTile(pos.x.round.int, pos.y.round.int) 284 | 285 | proc isSolid(map: Map, x, y: int): bool = 286 | map.getTile(x, y) notin {air, start, finish} 287 | 288 | proc isSolid(map: Map, point: Point2d): bool = 289 | map.isSolid(point.x.round.int, point.y.round.int) 290 | 291 | proc onGround(map: Map, pos: Point2d, size: Vector2d): bool = 292 | let size = size * 0.5 293 | result = 294 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y + 1)) or 295 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y + 1)) 296 | 297 | proc testBox(map: Map, pos: Point2d, size: Vector2d): bool = 298 | let size = size * 0.5 299 | result = 300 | map.isSolid(point2d(pos.x - size.x, pos.y - size.y)) or 301 | map.isSolid(point2d(pos.x + size.x, pos.y - size.y)) or 302 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y)) or 303 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y)) 304 | 305 | proc moveBox(map: Map, pos: var Point2d, vel: var Vector2d, 306 | size: Vector2d): set[Collision] {.discardable.} = 307 | let distance = vel.len 308 | let maximum = distance.int 309 | 310 | if distance < 0: 311 | return 312 | 313 | let fraction = 1.0 / float(maximum + 1) 314 | 315 | for i in 0 .. maximum: 316 | var newPos = pos + vel * fraction 317 | 318 | if map.testBox(newPos, size): 319 | var hit = false 320 | 321 | if map.testBox(point2d(pos.x, newPos.y), size): 322 | result.incl Collision.y 323 | newPos.y = pos.y 324 | vel.y = 0 325 | hit = true 326 | 327 | if map.testBox(point2d(newPos.x, pos.y), size): 328 | result.incl Collision.x 329 | newPos.x = pos.x 330 | vel.x = 0 331 | hit = true 332 | 333 | if not hit: 334 | result.incl Collision.corner 335 | newPos = pos 336 | vel = vector2d(0, 0) 337 | 338 | pos = newPos 339 | 340 | proc physics(game: Game) = 341 | if game.inputs[Input.restart]: 342 | game.player.restartPlayer() 343 | 344 | let ground = game.map.onGround(game.player.pos, playerSize) 345 | 346 | if game.inputs[Input.jump]: 347 | if ground: 348 | game.player.vel.y = -21 349 | 350 | let direction = float(game.inputs[Input.right].int - 351 | game.inputs[Input.left].int) 352 | 353 | game.player.vel.y += 0.75 354 | if ground: 355 | game.player.vel.x = 0.5 * game.player.vel.x + 4.0 * direction 356 | else: 357 | game.player.vel.x = 0.95 * game.player.vel.x + 2.0 * direction 358 | game.player.vel.x = clamp(game.player.vel.x, -8, 8) 359 | 360 | game.map.moveBox(game.player.pos, game.player.vel, playerSize) 361 | 362 | proc moveCamera(game: Game) = 363 | const halfWin = float(windowSize.x div 2) 364 | when defined(fluidCamera): 365 | let dist = game.camera.x - game.player.pos.x + halfWin 366 | game.camera.x -= 0.05 * dist 367 | elif defined(innerCamera): 368 | let 369 | leftArea = game.player.pos.x - halfWin - 100 370 | rightArea = game.player.pos.x - halfWin + 100 371 | game.camera.x = clamp(game.camera.x, leftArea, rightArea) 372 | else: 373 | game.camera.x = game.player.pos.x - halfWin 374 | 375 | proc logic(game: Game, tick: int) = 376 | template time: untyped = game.player.time 377 | case game.map.getTile(game.player.pos) 378 | of start: 379 | time.begin = tick 380 | of finish: 381 | if time.begin >= 0: 382 | time.finish = tick - time.begin 383 | time.begin = -1 384 | if time.best < 0 or time.finish < time.best: 385 | time.best = time.finish 386 | else: discard 387 | 388 | proc main = 389 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 390 | "SDL2 initialization failed" 391 | 392 | # defer blocks get called at the end of the procedure, even if an 393 | # exception has been thrown 394 | defer: sdl2.quit() 395 | 396 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 397 | "Linear texture filtering could not be enabled" 398 | 399 | const imgFlags: cint = IMG_INIT_PNG 400 | sdlFailIf(image.init(imgFlags) != imgFlags): 401 | "SDL2 Image initialization failed" 402 | defer: image.quit() 403 | 404 | sdlFailIf(ttfInit() == SdlError): 405 | "SDL2 TTF initialization failed" 406 | defer: ttfQuit() 407 | 408 | let window = createWindow(title = "Our own 2D platformer", 409 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 410 | w = windowSize.x, h = windowSize.y, flags = SDL_WINDOW_SHOWN) 411 | sdlFailIf window.isNil: "Window could not be created" 412 | defer: window.destroy() 413 | 414 | let renderer = window.createRenderer(index = -1, 415 | flags = Renderer_Accelerated or Renderer_PresentVsync) 416 | sdlFailIf renderer.isNil: "Renderer could not be created" 417 | defer: renderer.destroy() 418 | 419 | # Set the default color to use for drawing 420 | renderer.setDrawColor(r = 110, g = 132, b = 174) 421 | 422 | var 423 | game = newGame(renderer) 424 | startTime = epochTime() 425 | lastTick = 0 426 | 427 | # Game loop, draws each frame 428 | while not game.inputs[Input.quit]: 429 | game.handleInput() 430 | 431 | let newTick = int((epochTime() - startTime) * 50) 432 | for tick in lastTick+1 .. newTick: 433 | game.physics() 434 | game.moveCamera() 435 | game.logic(tick) 436 | lastTick = newTick 437 | 438 | game.render(lastTick) 439 | 440 | main() 441 | -------------------------------------------------------------------------------- /platformer.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.1" 4 | author = "Dennis Felsing" 5 | description = "An example 2D platform game with SDL2" 6 | license = "MIT" 7 | 8 | bin = @["platformer"] 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 0.10.0" 13 | requires "sdl2 >= 1.1" 14 | requires "strfmt >= 0.6" 15 | requires "basic2d >= 0.1.0" 16 | 17 | task tests, "Compile all tutorial steps": 18 | for i in 1..9: 19 | exec "nim c tutorial/platformer_part" & $i 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Writing a 2D Platform Game in Nim with SDL2 [![Build Status](https://circleci.com/gh/def-/nim-platformer.png)](https://circleci.com/gh/def-/nim-platformer) 2 | 3 | Code for [Writing a 2D Platform Game in Nim with SDL2](https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/). 4 | 5 | ## Building 6 | 7 | `nimble build` or `nim -d:release c platformer` both build a `platformer` 8 | binary file. 9 | 10 | ## Installation 11 | 12 | After `nimble install` the `platformer` binary is located in `~/.nimble/bin`. 13 | 14 | ## Download 15 | 16 | The resulting binaries can be downloaded here: [Win64](https://hookrace.net/public/platformer/platformer_1.0_win64.zip), [Win32](https://hookrace.net/public/platformer/platformer_1.0_win32.zip), [Linux x86_64](https://hookrace.net/public/platformer/platformer_1.0_linux_x86_64.tar.gz), [Linux x86](https://hookrace.net/public/platformer/platformer_1.0_linux_x86.tar.gz) 17 | 18 | ## Tutorial Steps 19 | 20 | All the tutorial steps can be compiled with `nimble tests`. 21 | -------------------------------------------------------------------------------- /release.nim: -------------------------------------------------------------------------------- 1 | import os, strfmt 2 | 3 | const 4 | app = "platformer" 5 | version = "1.0" 6 | 7 | builds = [ 8 | (name: "linux_x86", os: "linux", cpu: "i386", 9 | args: "--passC:-m32 --passL:-m32"), 10 | (name: "linux_x86_64", os: "linux", cpu: "amd64", 11 | args: "--passC:-Iglibc-hack"), 12 | (name: "win32", os: "windows", cpu: "i386", 13 | args: "--gcc.exe:i686-w64-mingw32-gcc --gcc.linkerexe:i686-w64-mingw32-gcc"), 14 | (name: "win64", os: "windows", cpu: "amd64", 15 | args: "--gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc"), 16 | ] 17 | 18 | removeDir "builds" 19 | 20 | for name, os, cpu, args in builds.items: 21 | let 22 | dirName = app & "_" & version & "_" & name 23 | dir = "builds" / dirName 24 | exeExt = if os == "windows": ".exe" else: "" 25 | bin = dir / app & exeExt 26 | 27 | createDir dir 28 | if execShellCmd(interp"nim --cpu:${cpu} --os:${os} ${args} -d:release -o:${bin} c ${app}") != 0: quit 1 29 | if execShellCmd(interp"strip -s ${bin}") != 0: quit 1 30 | copyDir("data", dir / "data") 31 | if os == "windows": copyDir("libs" / name, dir) 32 | setCurrentDir "builds" 33 | if os == "windows": 34 | if execShellCmd(interp"zip -9r ${dirName}.zip ${dirName}") != 0: quit 1 35 | else: 36 | if execShellCmd(interp"tar cfz ${dirName}.tar.gz ${dirName}") != 0: quit 1 37 | setCurrentDir ".." 38 | -------------------------------------------------------------------------------- /tutorial/DejaVuSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/tutorial/DejaVuSans.ttf -------------------------------------------------------------------------------- /tutorial/default.map: -------------------------------------------------------------------------------- 1 | 0 0 0 0 78 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 0 0 2 | 4 5 0 0 78 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 4 5 3 | 20 21 0 0 78 0 0 0 0 0 0 0 0 0 0 0 0 0 4 5 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 20 21 4 | 20 21 0 0 78 0 0 0 0 0 0 0 0 4 5 0 0 0 36 37 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 20 21 5 | 20 21 0 0 78 0 0 0 0 0 0 0 0 20 21 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 110 0 0 20 21 6 | 20 21 0 0 78 0 0 0 0 0 0 0 0 20 21 0 0 0 0 0 0 0 4 16 16 16 16 5 0 0 0 0 110 0 0 20 21 7 | 20 21 0 0 78 0 0 4 5 0 0 0 0 20 21 0 0 0 0 0 0 0 36 52 54 53 52 37 0 0 0 0 110 0 0 20 21 8 | 20 21 0 0 78 0 0 20 21 0 0 0 0 20 21 0 0 0 0 0 0 0 0 0 20 21 0 0 0 0 0 0 110 0 0 20 21 9 | 20 38 0 0 78 0 0 22 38 0 0 0 0 22 38 0 0 0 0 0 0 0 0 0 22 38 0 0 0 0 0 0 110 0 0 22 21 10 | 20 49 16 16 16 16 16 48 49 16 16 16 16 48 49 16 16 16 16 16 16 16 16 16 48 49 16 16 16 16 16 16 16 16 16 48 21 11 | 36 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 52 37 12 | -------------------------------------------------------------------------------- /tutorial/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/tutorial/grass.png -------------------------------------------------------------------------------- /tutorial/platformer_part1.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#1.-first-running-program 2 | 3 | import sdl2 4 | 5 | type SDLException = object of Exception 6 | 7 | template sdlFailIf(cond: typed, reason: string) = 8 | if cond: raise SDLException.newException( 9 | reason & ", SDL error: " & $getError()) 10 | 11 | proc main = 12 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 13 | "SDL2 initialization failed" 14 | 15 | # defer blocks get called at the end of the procedure, even if an 16 | # exception has been thrown 17 | defer: sdl2.quit() 18 | 19 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 20 | "Linear texture filtering could not be enabled" 21 | 22 | let window = createWindow(title = "Our own 2D platformer", 23 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 24 | w = 1280, h = 720, flags = SDL_WINDOW_SHOWN) 25 | sdlFailIf window.isNil: "Window could not be created" 26 | defer: window.destroy() 27 | 28 | let renderer = window.createRenderer(index = -1, 29 | flags = Renderer_Accelerated or Renderer_PresentVsync) 30 | sdlFailIf renderer.isNil: "Renderer could not be created" 31 | defer: renderer.destroy() 32 | 33 | # Set the default color to use for drawing 34 | renderer.setDrawColor(r = 110, g = 132, b = 174) 35 | 36 | # Game loop, draws each frame 37 | while true: 38 | # Draw over all drawings of the last frame with the default color 39 | renderer.clear() 40 | # Show the result on screen 41 | renderer.present() 42 | 43 | main() 44 | -------------------------------------------------------------------------------- /tutorial/platformer_part2.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#2.-user-input 2 | 3 | import sdl2 4 | 5 | type 6 | SDLException = object of Exception 7 | 8 | Input {.pure.} = enum none, left, right, jump, restart, quit 9 | 10 | Game = ref object 11 | inputs: array[Input, bool] 12 | renderer: RendererPtr 13 | 14 | template sdlFailIf(cond: typed, reason: string) = 15 | if cond: raise SDLException.newException( 16 | reason & ", SDL error: " & $getError()) 17 | 18 | proc newGame(renderer: RendererPtr): Game = 19 | new result 20 | result.renderer = renderer 21 | 22 | proc toInput(key: Scancode): Input = 23 | case key 24 | of SDL_SCANCODE_A: Input.left 25 | of SDL_SCANCODE_D: Input.right 26 | of SDL_SCANCODE_SPACE: Input.jump 27 | of SDL_SCANCODE_R: Input.restart 28 | of SDL_SCANCODE_Q: Input.quit 29 | else: Input.none 30 | 31 | proc handleInput(game: Game) = 32 | var event = defaultEvent 33 | while pollEvent(event): 34 | case event.kind 35 | of QuitEvent: 36 | game.inputs[Input.quit] = true 37 | of KeyDown: 38 | game.inputs[event.key.keysym.scancode.toInput] = true 39 | of KeyUp: 40 | game.inputs[event.key.keysym.scancode.toInput] = false 41 | else: 42 | discard 43 | 44 | proc render(game: Game) = 45 | # Draw over all drawings of the last frame with the default color 46 | game.renderer.clear() 47 | # Show the result on screen 48 | game.renderer.present() 49 | 50 | proc main = 51 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 52 | "SDL2 initialization failed" 53 | 54 | # defer blocks get called at the end of the procedure, even if an 55 | # exception has been thrown 56 | defer: sdl2.quit() 57 | 58 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 59 | "Linear texture filtering could not be enabled" 60 | 61 | let window = createWindow(title = "Our own 2D platformer", 62 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 63 | w = 1280, h = 720, flags = SDL_WINDOW_SHOWN) 64 | sdlFailIf window.isNil: "Window could not be created" 65 | defer: window.destroy() 66 | 67 | let renderer = window.createRenderer(index = -1, 68 | flags = Renderer_Accelerated or Renderer_PresentVsync) 69 | sdlFailIf renderer.isNil: "Renderer could not be created" 70 | defer: renderer.destroy() 71 | 72 | # Set the default color to use for drawing 73 | renderer.setDrawColor(r = 110, g = 132, b = 174) 74 | 75 | var game = newGame(renderer) 76 | 77 | # Game loop, draws each frame 78 | while not game.inputs[Input.quit]: 79 | game.handleInput() 80 | game.render() 81 | 82 | main() 83 | -------------------------------------------------------------------------------- /tutorial/platformer_part3.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#3.-displaying-graphics 2 | 3 | import sdl2, sdl2/image, basic2d 4 | 5 | type 6 | SDLException = object of Exception 7 | 8 | Input {.pure.} = enum none, left, right, jump, restart, quit 9 | 10 | Player = ref object 11 | texture: TexturePtr 12 | pos: Point2d 13 | vel: Vector2d 14 | 15 | Game = ref object 16 | inputs: array[Input, bool] 17 | renderer: RendererPtr 18 | player: Player 19 | camera: Vector2d 20 | 21 | template sdlFailIf(cond: typed, reason: string) = 22 | if cond: raise SDLException.newException( 23 | reason & ", SDL error: " & $getError()) 24 | 25 | proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) = 26 | let 27 | x = pos.x.cint 28 | y = pos.y.cint 29 | 30 | var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [ 31 | (rect(192, 64, 64, 32), rect(x-60, y, 96, 48), 32 | SDL_FLIP_NONE), # back feet shadow 33 | (rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96), 34 | SDL_FLIP_NONE), # body shadow 35 | (rect(192, 64, 64, 32), rect(x-36, y, 96, 48), 36 | SDL_FLIP_NONE), # front feet shadow 37 | (rect(192, 32, 64, 32), rect(x-60, y, 96, 48), 38 | SDL_FLIP_NONE), # back feet 39 | (rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96), 40 | SDL_FLIP_NONE), # body 41 | (rect(192, 32, 64, 32), rect(x-36, y, 96, 48), 42 | SDL_FLIP_NONE), # front feet 43 | (rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36), 44 | SDL_FLIP_NONE), # left eye 45 | (rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36), 46 | SDL_FLIP_HORIZONTAL) # right eye 47 | ] 48 | 49 | for part in bodyParts.mitems: 50 | renderer.copyEx(texture, part.source, part.dest, angle = 0.0, 51 | center = nil, flip = part.flip) 52 | 53 | proc restartPlayer(player: Player) = 54 | player.pos = point2d(170, 500) 55 | player.vel = vector2d(0, 0) 56 | 57 | proc newPlayer(texture: TexturePtr): Player = 58 | new result 59 | result.restartPlayer() 60 | result.texture = texture 61 | 62 | proc newGame(renderer: RendererPtr): Game = 63 | new result 64 | result.renderer = renderer 65 | result.player = newPlayer(renderer.loadTexture("player.png")) 66 | 67 | proc toInput(key: Scancode): Input = 68 | case key 69 | of SDL_SCANCODE_A: Input.left 70 | of SDL_SCANCODE_D: Input.right 71 | of SDL_SCANCODE_SPACE: Input.jump 72 | of SDL_SCANCODE_R: Input.restart 73 | of SDL_SCANCODE_Q: Input.quit 74 | else: Input.none 75 | 76 | proc handleInput(game: Game) = 77 | var event = defaultEvent 78 | while pollEvent(event): 79 | case event.kind 80 | of QuitEvent: 81 | game.inputs[Input.quit] = true 82 | of KeyDown: 83 | game.inputs[event.key.keysym.scancode.toInput] = true 84 | of KeyUp: 85 | game.inputs[event.key.keysym.scancode.toInput] = false 86 | else: 87 | discard 88 | 89 | proc render(game: Game) = 90 | # Draw over all drawings of the last frame with the default color 91 | game.renderer.clear() 92 | # Actual drawing here 93 | game.renderer.renderTee(game.player.texture, game.player.pos - game.camera) 94 | # Show the result on screen 95 | game.renderer.present() 96 | 97 | proc main = 98 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 99 | "SDL2 initialization failed" 100 | 101 | # defer blocks get called at the end of the procedure, even if an 102 | # exception has been thrown 103 | defer: sdl2.quit() 104 | 105 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 106 | "Linear texture filtering could not be enabled" 107 | 108 | const imgFlags: cint = IMG_INIT_PNG 109 | sdlFailIf(image.init(imgFlags) != imgFlags): 110 | "SDL2 Image initialization failed" 111 | defer: image.quit() 112 | 113 | let window = createWindow(title = "Our own 2D platformer", 114 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 115 | w = 1280, h = 720, flags = SDL_WINDOW_SHOWN) 116 | sdlFailIf window.isNil: "Window could not be created" 117 | defer: window.destroy() 118 | 119 | let renderer = window.createRenderer(index = -1, 120 | flags = Renderer_Accelerated or Renderer_PresentVsync) 121 | sdlFailIf renderer.isNil: "Renderer could not be created" 122 | defer: renderer.destroy() 123 | 124 | # Set the default color to use for drawing 125 | renderer.setDrawColor(r = 110, g = 132, b = 174) 126 | 127 | var game = newGame(renderer) 128 | 129 | # Game loop, draws each frame 130 | while not game.inputs[Input.quit]: 131 | game.handleInput() 132 | game.render() 133 | 134 | main() 135 | -------------------------------------------------------------------------------- /tutorial/platformer_part4.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#4.-tile-map 2 | 3 | import sdl2, sdl2/image, basic2d, strutils 4 | 5 | type 6 | SDLException = object of Exception 7 | 8 | Input {.pure.} = enum none, left, right, jump, restart, quit 9 | 10 | Player = ref object 11 | texture: TexturePtr 12 | pos: Point2d 13 | vel: Vector2d 14 | 15 | Map = ref object 16 | texture: TexturePtr 17 | width, height: int 18 | tiles: seq[uint8] 19 | 20 | Game = ref object 21 | inputs: array[Input, bool] 22 | renderer: RendererPtr 23 | player: Player 24 | map: Map 25 | camera: Vector2d 26 | 27 | const 28 | tilesPerRow = 16 29 | tileSize: Point = (64.cint, 64.cint) 30 | 31 | template sdlFailIf(cond: typed, reason: string) = 32 | if cond: raise SDLException.newException( 33 | reason & ", SDL error: " & $getError()) 34 | 35 | proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) = 36 | let 37 | x = pos.x.cint 38 | y = pos.y.cint 39 | 40 | var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [ 41 | (rect(192, 64, 64, 32), rect(x-60, y, 96, 48), 42 | SDL_FLIP_NONE), # back feet shadow 43 | (rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96), 44 | SDL_FLIP_NONE), # body shadow 45 | (rect(192, 64, 64, 32), rect(x-36, y, 96, 48), 46 | SDL_FLIP_NONE), # front feet shadow 47 | (rect(192, 32, 64, 32), rect(x-60, y, 96, 48), 48 | SDL_FLIP_NONE), # back feet 49 | (rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96), 50 | SDL_FLIP_NONE), # body 51 | (rect(192, 32, 64, 32), rect(x-36, y, 96, 48), 52 | SDL_FLIP_NONE), # front feet 53 | (rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36), 54 | SDL_FLIP_NONE), # left eye 55 | (rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36), 56 | SDL_FLIP_HORIZONTAL) # right eye 57 | ] 58 | 59 | for part in bodyParts.mitems: 60 | renderer.copyEx(texture, part.source, part.dest, angle = 0.0, 61 | center = nil, flip = part.flip) 62 | 63 | proc renderMap(renderer: RendererPtr, map: Map, camera: Vector2d) = 64 | var 65 | clip = rect(0, 0, tileSize.x, tileSize.y) 66 | dest = rect(0, 0, tileSize.x, tileSize.y) 67 | 68 | for i, tileNr in map.tiles: 69 | if tileNr == 0: continue 70 | 71 | clip.x = cint(tileNr mod tilesPerRow) * tileSize.x 72 | clip.y = cint(tileNr div tilesPerRow) * tileSize.y 73 | dest.x = cint(i mod map.width) * tileSize.x - camera.x.cint 74 | dest.y = cint(i div map.width) * tileSize.y - camera.y.cint 75 | 76 | renderer.copy(map.texture, unsafeAddr clip, unsafeAddr dest) 77 | 78 | proc restartPlayer(player: Player) = 79 | player.pos = point2d(170, 500) 80 | player.vel = vector2d(0, 0) 81 | 82 | proc newPlayer(texture: TexturePtr): Player = 83 | new result 84 | result.texture = texture 85 | result.restartPlayer() 86 | 87 | proc newMap(texture: TexturePtr, file: string): Map = 88 | new result 89 | result.texture = texture 90 | result.tiles = @[] 91 | 92 | for line in file.lines: 93 | var width = 0 94 | for word in line.split(' '): 95 | if word == "": continue 96 | let value = parseUInt(word) 97 | if value > uint(uint8.high): 98 | raise ValueError.newException( 99 | "Invalid value " & word & " in map " & file) 100 | result.tiles.add value.uint8 101 | inc width 102 | 103 | if result.width > 0 and result.width != width: 104 | raise ValueError.newException( 105 | "Incompatible line length in map " & file) 106 | result.width = width 107 | inc result.height 108 | 109 | proc newGame(renderer: RendererPtr): Game = 110 | new result 111 | result.renderer = renderer 112 | result.player = newPlayer(renderer.loadTexture("player.png")) 113 | result.map = newMap(renderer.loadTexture("grass.png"), "default.map") 114 | 115 | proc toInput(key: Scancode): Input = 116 | case key 117 | of SDL_SCANCODE_A: Input.left 118 | of SDL_SCANCODE_D: Input.right 119 | of SDL_SCANCODE_SPACE: Input.jump 120 | of SDL_SCANCODE_R: Input.restart 121 | of SDL_SCANCODE_Q: Input.quit 122 | else: Input.none 123 | 124 | proc handleInput(game: Game) = 125 | var event = defaultEvent 126 | while pollEvent(event): 127 | case event.kind 128 | of QuitEvent: 129 | game.inputs[Input.quit] = true 130 | of KeyDown: 131 | game.inputs[event.key.keysym.scancode.toInput] = true 132 | of KeyUp: 133 | game.inputs[event.key.keysym.scancode.toInput] = false 134 | else: 135 | discard 136 | 137 | proc render(game: Game) = 138 | # Draw over all drawings of the last frame with the default color 139 | game.renderer.clear() 140 | # Actual drawing here 141 | game.renderer.renderTee(game.player.texture, game.player.pos - game.camera) 142 | game.renderer.renderMap(game.map, game.camera) 143 | # Show the result on screen 144 | game.renderer.present() 145 | 146 | proc main = 147 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 148 | "SDL2 initialization failed" 149 | 150 | # defer blocks get called at the end of the procedure, even if an 151 | # exception has been thrown 152 | defer: sdl2.quit() 153 | 154 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 155 | "Linear texture filtering could not be enabled" 156 | 157 | const imgFlags: cint = IMG_INIT_PNG 158 | sdlFailIf(image.init(imgFlags) != imgFlags): 159 | "SDL2 Image initialization failed" 160 | defer: image.quit() 161 | 162 | let window = createWindow(title = "Our own 2D platformer", 163 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 164 | w = 1280, h = 720, flags = SDL_WINDOW_SHOWN) 165 | sdlFailIf window.isNil: "Window could not be created" 166 | defer: window.destroy() 167 | 168 | let renderer = window.createRenderer(index = -1, 169 | flags = Renderer_Accelerated or Renderer_PresentVsync) 170 | sdlFailIf renderer.isNil: "Renderer could not be created" 171 | defer: renderer.destroy() 172 | 173 | # Set the default color to use for drawing 174 | renderer.setDrawColor(r = 110, g = 132, b = 174) 175 | 176 | var game = newGame(renderer) 177 | 178 | # Game loop, draws each frame 179 | while not game.inputs[Input.quit]: 180 | game.handleInput() 181 | game.render() 182 | 183 | main() 184 | -------------------------------------------------------------------------------- /tutorial/platformer_part5.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#5.-physics-and-collisions 2 | 3 | import sdl2, sdl2/image, basic2d, strutils, times, math 4 | 5 | type 6 | SDLException = object of Exception 7 | 8 | Input {.pure.} = enum none, left, right, jump, restart, quit 9 | 10 | Collision {.pure.} = enum x, y, corner 11 | 12 | Player = ref object 13 | texture: TexturePtr 14 | pos: Point2d 15 | vel: Vector2d 16 | 17 | Map = ref object 18 | texture: TexturePtr 19 | width, height: int 20 | tiles: seq[uint8] 21 | 22 | Game = ref object 23 | inputs: array[Input, bool] 24 | renderer: RendererPtr 25 | player: Player 26 | map: Map 27 | camera: Vector2d 28 | 29 | const 30 | tilesPerRow = 16 31 | tileSize: Point = (64.cint, 64.cint) 32 | 33 | playerSize = vector2d(64, 64) 34 | 35 | air = 0 36 | start = 78 37 | finish = 110 38 | 39 | template sdlFailIf(cond: typed, reason: string) = 40 | if cond: raise SDLException.newException( 41 | reason & ", SDL error: " & $getError()) 42 | 43 | proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) = 44 | let 45 | x = pos.x.cint 46 | y = pos.y.cint 47 | 48 | var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [ 49 | (rect(192, 64, 64, 32), rect(x-60, y, 96, 48), 50 | SDL_FLIP_NONE), # back feet shadow 51 | (rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96), 52 | SDL_FLIP_NONE), # body shadow 53 | (rect(192, 64, 64, 32), rect(x-36, y, 96, 48), 54 | SDL_FLIP_NONE), # front feet shadow 55 | (rect(192, 32, 64, 32), rect(x-60, y, 96, 48), 56 | SDL_FLIP_NONE), # back feet 57 | (rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96), 58 | SDL_FLIP_NONE), # body 59 | (rect(192, 32, 64, 32), rect(x-36, y, 96, 48), 60 | SDL_FLIP_NONE), # front feet 61 | (rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36), 62 | SDL_FLIP_NONE), # left eye 63 | (rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36), 64 | SDL_FLIP_HORIZONTAL) # right eye 65 | ] 66 | 67 | for part in bodyParts.mitems: 68 | renderer.copyEx(texture, part.source, part.dest, angle = 0.0, 69 | center = nil, flip = part.flip) 70 | 71 | proc renderMap(renderer: RendererPtr, map: Map, camera: Vector2d) = 72 | var 73 | clip = rect(0, 0, tileSize.x, tileSize.y) 74 | dest = rect(0, 0, tileSize.x, tileSize.y) 75 | 76 | for i, tileNr in map.tiles: 77 | if tileNr == 0: continue 78 | 79 | clip.x = cint(tileNr mod tilesPerRow) * tileSize.x 80 | clip.y = cint(tileNr div tilesPerRow) * tileSize.y 81 | dest.x = cint(i mod map.width) * tileSize.x - camera.x.cint 82 | dest.y = cint(i div map.width) * tileSize.y - camera.y.cint 83 | 84 | renderer.copy(map.texture, unsafeAddr clip, unsafeAddr dest) 85 | 86 | proc restartPlayer(player: Player) = 87 | player.pos = point2d(170, 500) 88 | player.vel = vector2d(0, 0) 89 | 90 | proc newPlayer(texture: TexturePtr): Player = 91 | new result 92 | result.texture = texture 93 | result.restartPlayer() 94 | 95 | proc newMap(texture: TexturePtr, file: string): Map = 96 | new result 97 | result.texture = texture 98 | result.tiles = @[] 99 | 100 | for line in file.lines: 101 | var width = 0 102 | for word in line.split(' '): 103 | if word == "": continue 104 | let value = parseUInt(word) 105 | if value > uint(uint8.high): 106 | raise ValueError.newException( 107 | "Invalid value " & word & " in map " & file) 108 | result.tiles.add value.uint8 109 | inc width 110 | 111 | if result.width > 0 and result.width != width: 112 | raise ValueError.newException( 113 | "Incompatible line length in map " & file) 114 | result.width = width 115 | inc result.height 116 | 117 | proc newGame(renderer: RendererPtr): Game = 118 | new result 119 | result.renderer = renderer 120 | result.player = newPlayer(renderer.loadTexture("player.png")) 121 | result.map = newMap(renderer.loadTexture("grass.png"), "default.map") 122 | 123 | proc toInput(key: Scancode): Input = 124 | case key 125 | of SDL_SCANCODE_A: Input.left 126 | of SDL_SCANCODE_D: Input.right 127 | of SDL_SCANCODE_SPACE: Input.jump 128 | of SDL_SCANCODE_R: Input.restart 129 | of SDL_SCANCODE_Q: Input.quit 130 | else: Input.none 131 | 132 | proc handleInput(game: Game) = 133 | var event = defaultEvent 134 | while pollEvent(event): 135 | case event.kind 136 | of QuitEvent: 137 | game.inputs[Input.quit] = true 138 | of KeyDown: 139 | game.inputs[event.key.keysym.scancode.toInput] = true 140 | of KeyUp: 141 | game.inputs[event.key.keysym.scancode.toInput] = false 142 | else: 143 | discard 144 | 145 | proc render(game: Game) = 146 | # Draw over all drawings of the last frame with the default color 147 | game.renderer.clear() 148 | # Actual drawing here 149 | game.renderer.renderTee(game.player.texture, game.player.pos - game.camera) 150 | game.renderer.renderMap(game.map, game.camera) 151 | # Show the result on screen 152 | game.renderer.present() 153 | 154 | proc getTile(map: Map, x, y: int): uint8 = 155 | let 156 | nx = clamp(x div tileSize.x, 0, map.width - 1) 157 | ny = clamp(y div tileSize.y, 0, map.height - 1) 158 | pos = ny * map.width + nx 159 | 160 | map.tiles[pos] 161 | 162 | proc isSolid(map: Map, x, y: int): bool = 163 | map.getTile(x, y) notin {air, start, finish} 164 | 165 | proc isSolid(map: Map, point: Point2d): bool = 166 | map.isSolid(point.x.round.int, point.y.round.int) 167 | 168 | proc onGround(map: Map, pos: Point2d, size: Vector2d): bool = 169 | let size = size * 0.5 170 | result = 171 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y + 1)) or 172 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y + 1)) 173 | 174 | proc testBox(map: Map, pos: Point2d, size: Vector2d): bool = 175 | let size = size * 0.5 176 | result = 177 | map.isSolid(point2d(pos.x - size.x, pos.y - size.y)) or 178 | map.isSolid(point2d(pos.x + size.x, pos.y - size.y)) or 179 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y)) or 180 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y)) 181 | 182 | proc moveBox(map: Map, pos: var Point2d, vel: var Vector2d, 183 | size: Vector2d): set[Collision] {.discardable.} = 184 | let distance = vel.len 185 | let maximum = distance.int 186 | 187 | if distance < 0: 188 | return 189 | 190 | let fraction = 1.0 / float(maximum + 1) 191 | 192 | for i in 0 .. maximum: 193 | var newPos = pos + vel * fraction 194 | 195 | if map.testBox(newPos, size): 196 | var hit = false 197 | 198 | if map.testBox(point2d(pos.x, newPos.y), size): 199 | result.incl Collision.y 200 | newPos.y = pos.y 201 | vel.y = 0 202 | hit = true 203 | 204 | if map.testBox(point2d(newPos.x, pos.y), size): 205 | result.incl Collision.x 206 | newPos.x = pos.x 207 | vel.x = 0 208 | hit = true 209 | 210 | if not hit: 211 | result.incl Collision.corner 212 | newPos = pos 213 | vel = vector2d(0, 0) 214 | 215 | pos = newPos 216 | 217 | proc physics(game: Game) = 218 | if game.inputs[Input.restart]: 219 | game.player.restartPlayer() 220 | 221 | let ground = game.map.onGround(game.player.pos, playerSize) 222 | 223 | if game.inputs[Input.jump]: 224 | if ground: 225 | game.player.vel.y = -21 226 | 227 | let direction = float(game.inputs[Input.right].int - 228 | game.inputs[Input.left].int) 229 | 230 | game.player.vel.y += 0.75 231 | if ground: 232 | game.player.vel.x = 0.5 * game.player.vel.x + 4.0 * direction 233 | else: 234 | game.player.vel.x = 0.95 * game.player.vel.x + 2.0 * direction 235 | game.player.vel.x = clamp(game.player.vel.x, -8, 8) 236 | 237 | game.map.moveBox(game.player.pos, game.player.vel, playerSize) 238 | 239 | proc main = 240 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 241 | "SDL2 initialization failed" 242 | 243 | # defer blocks get called at the end of the procedure, even if an 244 | # exception has been thrown 245 | defer: sdl2.quit() 246 | 247 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 248 | "Linear texture filtering could not be enabled" 249 | 250 | const imgFlags: cint = IMG_INIT_PNG 251 | sdlFailIf(image.init(imgFlags) != imgFlags): 252 | "SDL2 Image initialization failed" 253 | defer: image.quit() 254 | 255 | let window = createWindow(title = "Our own 2D platformer", 256 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 257 | w = 1280, h = 720, flags = SDL_WINDOW_SHOWN) 258 | sdlFailIf window.isNil: "Window could not be created" 259 | defer: window.destroy() 260 | 261 | let renderer = window.createRenderer(index = -1, 262 | flags = Renderer_Accelerated or Renderer_PresentVsync) 263 | sdlFailIf renderer.isNil: "Renderer could not be created" 264 | defer: renderer.destroy() 265 | 266 | # Set the default color to use for drawing 267 | renderer.setDrawColor(r = 110, g = 132, b = 174) 268 | 269 | var 270 | game = newGame(renderer) 271 | startTime = epochTime() 272 | lastTick = 0 273 | 274 | # Game loop, draws each frame 275 | while not game.inputs[Input.quit]: 276 | game.handleInput() 277 | 278 | let newTick = int((epochTime() - startTime) * 50) 279 | for tick in lastTick+1 .. newTick: 280 | game.physics() 281 | lastTick = newTick 282 | 283 | game.render() 284 | 285 | main() 286 | -------------------------------------------------------------------------------- /tutorial/platformer_part6.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#6.-camera 2 | 3 | import sdl2, sdl2/image, basic2d, strutils, times, math 4 | 5 | type 6 | SDLException = object of Exception 7 | 8 | Input {.pure.} = enum none, left, right, jump, restart, quit 9 | 10 | Collision {.pure.} = enum x, y, corner 11 | 12 | Player = ref object 13 | texture: TexturePtr 14 | pos: Point2d 15 | vel: Vector2d 16 | 17 | Map = ref object 18 | texture: TexturePtr 19 | width, height: int 20 | tiles: seq[uint8] 21 | 22 | Game = ref object 23 | inputs: array[Input, bool] 24 | renderer: RendererPtr 25 | player: Player 26 | map: Map 27 | camera: Vector2d 28 | 29 | const 30 | windowSize: Point = (1280.cint, 720.cint) 31 | 32 | tilesPerRow = 16 33 | tileSize: Point = (64.cint, 64.cint) 34 | 35 | playerSize = vector2d(64, 64) 36 | 37 | air = 0 38 | start = 78 39 | finish = 110 40 | 41 | template sdlFailIf(cond: typed, reason: string) = 42 | if cond: raise SDLException.newException( 43 | reason & ", SDL error: " & $getError()) 44 | 45 | proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) = 46 | let 47 | x = pos.x.cint 48 | y = pos.y.cint 49 | 50 | var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [ 51 | (rect(192, 64, 64, 32), rect(x-60, y, 96, 48), 52 | SDL_FLIP_NONE), # back feet shadow 53 | (rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96), 54 | SDL_FLIP_NONE), # body shadow 55 | (rect(192, 64, 64, 32), rect(x-36, y, 96, 48), 56 | SDL_FLIP_NONE), # front feet shadow 57 | (rect(192, 32, 64, 32), rect(x-60, y, 96, 48), 58 | SDL_FLIP_NONE), # back feet 59 | (rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96), 60 | SDL_FLIP_NONE), # body 61 | (rect(192, 32, 64, 32), rect(x-36, y, 96, 48), 62 | SDL_FLIP_NONE), # front feet 63 | (rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36), 64 | SDL_FLIP_NONE), # left eye 65 | (rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36), 66 | SDL_FLIP_HORIZONTAL) # right eye 67 | ] 68 | 69 | for part in bodyParts.mitems: 70 | renderer.copyEx(texture, part.source, part.dest, angle = 0.0, 71 | center = nil, flip = part.flip) 72 | 73 | proc renderMap(renderer: RendererPtr, map: Map, camera: Vector2d) = 74 | var 75 | clip = rect(0, 0, tileSize.x, tileSize.y) 76 | dest = rect(0, 0, tileSize.x, tileSize.y) 77 | 78 | for i, tileNr in map.tiles: 79 | if tileNr == 0: continue 80 | 81 | clip.x = cint(tileNr mod tilesPerRow) * tileSize.x 82 | clip.y = cint(tileNr div tilesPerRow) * tileSize.y 83 | dest.x = cint(i mod map.width) * tileSize.x - camera.x.cint 84 | dest.y = cint(i div map.width) * tileSize.y - camera.y.cint 85 | 86 | renderer.copy(map.texture, unsafeAddr clip, unsafeAddr dest) 87 | 88 | proc restartPlayer(player: Player) = 89 | player.pos = point2d(170, 500) 90 | player.vel = vector2d(0, 0) 91 | 92 | proc newPlayer(texture: TexturePtr): Player = 93 | new result 94 | result.texture = texture 95 | result.restartPlayer() 96 | 97 | proc newMap(texture: TexturePtr, file: string): Map = 98 | new result 99 | result.texture = texture 100 | result.tiles = @[] 101 | 102 | for line in file.lines: 103 | var width = 0 104 | for word in line.split(' '): 105 | if word == "": continue 106 | let value = parseUInt(word) 107 | if value > uint(uint8.high): 108 | raise ValueError.newException( 109 | "Invalid value " & word & " in map " & file) 110 | result.tiles.add value.uint8 111 | inc width 112 | 113 | if result.width > 0 and result.width != width: 114 | raise ValueError.newException( 115 | "Incompatible line length in map " & file) 116 | result.width = width 117 | inc result.height 118 | 119 | proc newGame(renderer: RendererPtr): Game = 120 | new result 121 | result.renderer = renderer 122 | result.player = newPlayer(renderer.loadTexture("player.png")) 123 | result.map = newMap(renderer.loadTexture("grass.png"), "default.map") 124 | 125 | proc toInput(key: Scancode): Input = 126 | case key 127 | of SDL_SCANCODE_A: Input.left 128 | of SDL_SCANCODE_D: Input.right 129 | of SDL_SCANCODE_SPACE: Input.jump 130 | of SDL_SCANCODE_R: Input.restart 131 | of SDL_SCANCODE_Q: Input.quit 132 | else: Input.none 133 | 134 | proc handleInput(game: Game) = 135 | var event = defaultEvent 136 | while pollEvent(event): 137 | case event.kind 138 | of QuitEvent: 139 | game.inputs[Input.quit] = true 140 | of KeyDown: 141 | game.inputs[event.key.keysym.scancode.toInput] = true 142 | of KeyUp: 143 | game.inputs[event.key.keysym.scancode.toInput] = false 144 | else: 145 | discard 146 | 147 | proc render(game: Game) = 148 | # Draw over all drawings of the last frame with the default color 149 | game.renderer.clear() 150 | # Actual drawing here 151 | game.renderer.renderTee(game.player.texture, game.player.pos - game.camera) 152 | game.renderer.renderMap(game.map, game.camera) 153 | # Show the result on screen 154 | game.renderer.present() 155 | 156 | proc getTile(map: Map, x, y: int): uint8 = 157 | let 158 | nx = clamp(x div tileSize.x, 0, map.width - 1) 159 | ny = clamp(y div tileSize.y, 0, map.height - 1) 160 | pos = ny * map.width + nx 161 | 162 | map.tiles[pos] 163 | 164 | proc isSolid(map: Map, x, y: int): bool = 165 | map.getTile(x, y) notin {air, start, finish} 166 | 167 | proc isSolid(map: Map, point: Point2d): bool = 168 | map.isSolid(point.x.round.int, point.y.round.int) 169 | 170 | proc onGround(map: Map, pos: Point2d, size: Vector2d): bool = 171 | let size = size * 0.5 172 | result = 173 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y + 1)) or 174 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y + 1)) 175 | 176 | proc testBox(map: Map, pos: Point2d, size: Vector2d): bool = 177 | let size = size * 0.5 178 | result = 179 | map.isSolid(point2d(pos.x - size.x, pos.y - size.y)) or 180 | map.isSolid(point2d(pos.x + size.x, pos.y - size.y)) or 181 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y)) or 182 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y)) 183 | 184 | proc moveBox(map: Map, pos: var Point2d, vel: var Vector2d, 185 | size: Vector2d): set[Collision] {.discardable.} = 186 | let distance = vel.len 187 | let maximum = distance.int 188 | 189 | if distance < 0: 190 | return 191 | 192 | let fraction = 1.0 / float(maximum + 1) 193 | 194 | for i in 0 .. maximum: 195 | var newPos = pos + vel * fraction 196 | 197 | if map.testBox(newPos, size): 198 | var hit = false 199 | 200 | if map.testBox(point2d(pos.x, newPos.y), size): 201 | result.incl Collision.y 202 | newPos.y = pos.y 203 | vel.y = 0 204 | hit = true 205 | 206 | if map.testBox(point2d(newPos.x, pos.y), size): 207 | result.incl Collision.x 208 | newPos.x = pos.x 209 | vel.x = 0 210 | hit = true 211 | 212 | if not hit: 213 | result.incl Collision.corner 214 | newPos = pos 215 | vel = vector2d(0, 0) 216 | 217 | pos = newPos 218 | 219 | proc physics(game: Game) = 220 | if game.inputs[Input.restart]: 221 | game.player.restartPlayer() 222 | 223 | let ground = game.map.onGround(game.player.pos, playerSize) 224 | 225 | if game.inputs[Input.jump]: 226 | if ground: 227 | game.player.vel.y = -21 228 | 229 | let direction = float(game.inputs[Input.right].int - 230 | game.inputs[Input.left].int) 231 | 232 | game.player.vel.y += 0.75 233 | if ground: 234 | game.player.vel.x = 0.5 * game.player.vel.x + 4.0 * direction 235 | else: 236 | game.player.vel.x = 0.95 * game.player.vel.x + 2.0 * direction 237 | game.player.vel.x = clamp(game.player.vel.x, -8, 8) 238 | 239 | game.map.moveBox(game.player.pos, game.player.vel, playerSize) 240 | 241 | proc moveCamera(game: Game) = 242 | const halfWin = float(windowSize.x div 2) 243 | when defined(fluidCamera): 244 | let dist = game.camera.x - game.player.pos.x + halfWin 245 | game.camera.x -= 0.05 * dist 246 | elif defined(innerCamera): 247 | let 248 | leftArea = game.player.pos.x - halfWin - 100 249 | rightArea = game.player.pos.x - halfWin + 100 250 | game.camera.x = clamp(game.camera.x, leftArea, rightArea) 251 | else: 252 | game.camera.x = game.player.pos.x - halfWin 253 | 254 | proc main = 255 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 256 | "SDL2 initialization failed" 257 | 258 | # defer blocks get called at the end of the procedure, even if an 259 | # exception has been thrown 260 | defer: sdl2.quit() 261 | 262 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 263 | "Linear texture filtering could not be enabled" 264 | 265 | const imgFlags: cint = IMG_INIT_PNG 266 | sdlFailIf(image.init(imgFlags) != imgFlags): 267 | "SDL2 Image initialization failed" 268 | defer: image.quit() 269 | 270 | let window = createWindow(title = "Our own 2D platformer", 271 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 272 | w = windowSize.x, h = windowSize.y, flags = SDL_WINDOW_SHOWN) 273 | sdlFailIf window.isNil: "Window could not be created" 274 | defer: window.destroy() 275 | 276 | let renderer = window.createRenderer(index = -1, 277 | flags = Renderer_Accelerated or Renderer_PresentVsync) 278 | sdlFailIf renderer.isNil: "Renderer could not be created" 279 | defer: renderer.destroy() 280 | 281 | # Set the default color to use for drawing 282 | renderer.setDrawColor(r = 110, g = 132, b = 174) 283 | 284 | var 285 | game = newGame(renderer) 286 | startTime = epochTime() 287 | lastTick = 0 288 | 289 | # Game loop, draws each frame 290 | while not game.inputs[Input.quit]: 291 | game.handleInput() 292 | 293 | let newTick = int((epochTime() - startTime) * 50) 294 | for tick in lastTick+1 .. newTick: 295 | game.physics() 296 | game.moveCamera() 297 | lastTick = newTick 298 | 299 | game.render() 300 | 301 | main() 302 | -------------------------------------------------------------------------------- /tutorial/platformer_part7.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#7.-game-state 2 | 3 | import sdl2, sdl2/image, basic2d, strutils, times, math, strfmt 4 | 5 | type 6 | SDLException = object of Exception 7 | 8 | Input {.pure.} = enum none, left, right, jump, restart, quit 9 | 10 | Collision {.pure.} = enum x, y, corner 11 | 12 | Player = ref object 13 | texture: TexturePtr 14 | pos: Point2d 15 | vel: Vector2d 16 | time: Time 17 | 18 | Map = ref object 19 | texture: TexturePtr 20 | width, height: int 21 | tiles: seq[uint8] 22 | 23 | Time = ref object 24 | begin, finish, best: int 25 | 26 | Game = ref object 27 | inputs: array[Input, bool] 28 | renderer: RendererPtr 29 | player: Player 30 | map: Map 31 | camera: Vector2d 32 | 33 | const 34 | windowSize: Point = (1280.cint, 720.cint) 35 | 36 | tilesPerRow = 16 37 | tileSize: Point = (64.cint, 64.cint) 38 | 39 | playerSize = vector2d(64, 64) 40 | 41 | air = 0 42 | start = 78 43 | finish = 110 44 | 45 | template sdlFailIf(cond: typed, reason: string) = 46 | if cond: raise SDLException.newException( 47 | reason & ", SDL error: " & $getError()) 48 | 49 | proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) = 50 | let 51 | x = pos.x.cint 52 | y = pos.y.cint 53 | 54 | var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [ 55 | (rect(192, 64, 64, 32), rect(x-60, y, 96, 48), 56 | SDL_FLIP_NONE), # back feet shadow 57 | (rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96), 58 | SDL_FLIP_NONE), # body shadow 59 | (rect(192, 64, 64, 32), rect(x-36, y, 96, 48), 60 | SDL_FLIP_NONE), # front feet shadow 61 | (rect(192, 32, 64, 32), rect(x-60, y, 96, 48), 62 | SDL_FLIP_NONE), # back feet 63 | (rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96), 64 | SDL_FLIP_NONE), # body 65 | (rect(192, 32, 64, 32), rect(x-36, y, 96, 48), 66 | SDL_FLIP_NONE), # front feet 67 | (rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36), 68 | SDL_FLIP_NONE), # left eye 69 | (rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36), 70 | SDL_FLIP_HORIZONTAL) # right eye 71 | ] 72 | 73 | for part in bodyParts.mitems: 74 | renderer.copyEx(texture, part.source, part.dest, angle = 0.0, 75 | center = nil, flip = part.flip) 76 | 77 | proc renderMap(renderer: RendererPtr, map: Map, camera: Vector2d) = 78 | var 79 | clip = rect(0, 0, tileSize.x, tileSize.y) 80 | dest = rect(0, 0, tileSize.x, tileSize.y) 81 | 82 | for i, tileNr in map.tiles: 83 | if tileNr == 0: continue 84 | 85 | clip.x = cint(tileNr mod tilesPerRow) * tileSize.x 86 | clip.y = cint(tileNr div tilesPerRow) * tileSize.y 87 | dest.x = cint(i mod map.width) * tileSize.x - camera.x.cint 88 | dest.y = cint(i div map.width) * tileSize.y - camera.y.cint 89 | 90 | renderer.copy(map.texture, unsafeAddr clip, unsafeAddr dest) 91 | 92 | proc restartPlayer(player: Player) = 93 | player.pos = point2d(170, 500) 94 | player.vel = vector2d(0, 0) 95 | player.time.begin = -1 96 | player.time.finish = -1 97 | 98 | proc newTime: Time = 99 | new result 100 | result.finish = -1 101 | result.best = -1 102 | 103 | proc newPlayer(texture: TexturePtr): Player = 104 | new result 105 | result.texture = texture 106 | result.time = newTime() 107 | result.restartPlayer() 108 | 109 | proc newMap(texture: TexturePtr, file: string): Map = 110 | new result 111 | result.texture = texture 112 | result.tiles = @[] 113 | 114 | for line in file.lines: 115 | var width = 0 116 | for word in line.split(' '): 117 | if word == "": continue 118 | let value = parseUInt(word) 119 | if value > uint(uint8.high): 120 | raise ValueError.newException( 121 | "Invalid value " & word & " in map " & file) 122 | result.tiles.add value.uint8 123 | inc width 124 | 125 | if result.width > 0 and result.width != width: 126 | raise ValueError.newException( 127 | "Incompatible line length in map " & file) 128 | result.width = width 129 | inc result.height 130 | 131 | proc newGame(renderer: RendererPtr): Game = 132 | new result 133 | result.renderer = renderer 134 | result.player = newPlayer(renderer.loadTexture("player.png")) 135 | result.map = newMap(renderer.loadTexture("grass.png"), "default.map") 136 | 137 | proc toInput(key: Scancode): Input = 138 | case key 139 | of SDL_SCANCODE_A: Input.left 140 | of SDL_SCANCODE_D: Input.right 141 | of SDL_SCANCODE_SPACE: Input.jump 142 | of SDL_SCANCODE_R: Input.restart 143 | of SDL_SCANCODE_Q: Input.quit 144 | else: Input.none 145 | 146 | proc handleInput(game: Game) = 147 | var event = defaultEvent 148 | while pollEvent(event): 149 | case event.kind 150 | of QuitEvent: 151 | game.inputs[Input.quit] = true 152 | of KeyDown: 153 | game.inputs[event.key.keysym.scancode.toInput] = true 154 | of KeyUp: 155 | game.inputs[event.key.keysym.scancode.toInput] = false 156 | else: 157 | discard 158 | 159 | proc render(game: Game) = 160 | # Draw over all drawings of the last frame with the default color 161 | game.renderer.clear() 162 | # Actual drawing here 163 | game.renderer.renderTee(game.player.texture, game.player.pos - game.camera) 164 | game.renderer.renderMap(game.map, game.camera) 165 | # Show the result on screen 166 | game.renderer.present() 167 | 168 | proc getTile(map: Map, x, y: int): uint8 = 169 | let 170 | nx = clamp(x div tileSize.x, 0, map.width - 1) 171 | ny = clamp(y div tileSize.y, 0, map.height - 1) 172 | pos = ny * map.width + nx 173 | 174 | map.tiles[pos] 175 | 176 | proc getTile(map: Map, pos: Point2d): uint8 = 177 | map.getTile(pos.x.round.int, pos.y.round.int) 178 | 179 | proc isSolid(map: Map, x, y: int): bool = 180 | map.getTile(x, y) notin {air, start, finish} 181 | 182 | proc isSolid(map: Map, point: Point2d): bool = 183 | map.isSolid(point.x.round.int, point.y.round.int) 184 | 185 | proc onGround(map: Map, pos: Point2d, size: Vector2d): bool = 186 | let size = size * 0.5 187 | result = 188 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y + 1)) or 189 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y + 1)) 190 | 191 | proc testBox(map: Map, pos: Point2d, size: Vector2d): bool = 192 | let size = size * 0.5 193 | result = 194 | map.isSolid(point2d(pos.x - size.x, pos.y - size.y)) or 195 | map.isSolid(point2d(pos.x + size.x, pos.y - size.y)) or 196 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y)) or 197 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y)) 198 | 199 | proc moveBox(map: Map, pos: var Point2d, vel: var Vector2d, 200 | size: Vector2d): set[Collision] {.discardable.} = 201 | let distance = vel.len 202 | let maximum = distance.int 203 | 204 | if distance < 0: 205 | return 206 | 207 | let fraction = 1.0 / float(maximum + 1) 208 | 209 | for i in 0 .. maximum: 210 | var newPos = pos + vel * fraction 211 | 212 | if map.testBox(newPos, size): 213 | var hit = false 214 | 215 | if map.testBox(point2d(pos.x, newPos.y), size): 216 | result.incl Collision.y 217 | newPos.y = pos.y 218 | vel.y = 0 219 | hit = true 220 | 221 | if map.testBox(point2d(newPos.x, pos.y), size): 222 | result.incl Collision.x 223 | newPos.x = pos.x 224 | vel.x = 0 225 | hit = true 226 | 227 | if not hit: 228 | result.incl Collision.corner 229 | newPos = pos 230 | vel = vector2d(0, 0) 231 | 232 | pos = newPos 233 | 234 | proc physics(game: Game) = 235 | if game.inputs[Input.restart]: 236 | game.player.restartPlayer() 237 | 238 | let ground = game.map.onGround(game.player.pos, playerSize) 239 | 240 | if game.inputs[Input.jump]: 241 | if ground: 242 | game.player.vel.y = -21 243 | 244 | let direction = float(game.inputs[Input.right].int - 245 | game.inputs[Input.left].int) 246 | 247 | game.player.vel.y += 0.75 248 | if ground: 249 | game.player.vel.x = 0.5 * game.player.vel.x + 4.0 * direction 250 | else: 251 | game.player.vel.x = 0.95 * game.player.vel.x + 2.0 * direction 252 | game.player.vel.x = clamp(game.player.vel.x, -8, 8) 253 | 254 | game.map.moveBox(game.player.pos, game.player.vel, playerSize) 255 | 256 | proc moveCamera(game: Game) = 257 | const halfWin = float(windowSize.x div 2) 258 | when defined(fluidCamera): 259 | let dist = game.camera.x - game.player.pos.x + halfWin 260 | game.camera.x -= 0.05 * dist 261 | elif defined(innerCamera): 262 | let 263 | leftArea = game.player.pos.x - halfWin - 100 264 | rightArea = game.player.pos.x - halfWin + 100 265 | game.camera.x = clamp(game.camera.x, leftArea, rightArea) 266 | else: 267 | game.camera.x = game.player.pos.x - halfWin 268 | 269 | proc formatTime(ticks: int): string = 270 | let 271 | mins = (ticks div 50) div 60 272 | secs = (ticks div 50) mod 60 273 | cents = (ticks mod 50) * 2 274 | interp"${mins:02}:${secs:02}:${cents:02}" 275 | 276 | proc logic(game: Game, tick: int) = 277 | template time: untyped = game.player.time 278 | case game.map.getTile(game.player.pos) 279 | of start: 280 | time.begin = tick 281 | of finish: 282 | if time.begin >= 0: 283 | time.finish = tick - time.begin 284 | time.begin = -1 285 | if time.best < 0 or time.finish < time.best: 286 | time.best = time.finish 287 | echo "Finished in ", formatTime(time.finish) 288 | else: discard 289 | 290 | proc main = 291 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 292 | "SDL2 initialization failed" 293 | 294 | # defer blocks get called at the end of the procedure, even if an 295 | # exception has been thrown 296 | defer: sdl2.quit() 297 | 298 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 299 | "Linear texture filtering could not be enabled" 300 | 301 | const imgFlags: cint = IMG_INIT_PNG 302 | sdlFailIf(image.init(imgFlags) != imgFlags): 303 | "SDL2 Image initialization failed" 304 | defer: image.quit() 305 | 306 | let window = createWindow(title = "Our own 2D platformer", 307 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 308 | w = windowSize.x, h = windowSize.y, flags = SDL_WINDOW_SHOWN) 309 | sdlFailIf window.isNil: "Window could not be created" 310 | defer: window.destroy() 311 | 312 | let renderer = window.createRenderer(index = -1, 313 | flags = Renderer_Accelerated or Renderer_PresentVsync) 314 | sdlFailIf renderer.isNil: "Renderer could not be created" 315 | defer: renderer.destroy() 316 | 317 | # Set the default color to use for drawing 318 | renderer.setDrawColor(r = 110, g = 132, b = 174) 319 | 320 | var 321 | game = newGame(renderer) 322 | startTime = epochTime() 323 | lastTick = 0 324 | 325 | # Game loop, draws each frame 326 | while not game.inputs[Input.quit]: 327 | game.handleInput() 328 | 329 | let newTick = int((epochTime() - startTime) * 50) 330 | for tick in lastTick+1 .. newTick: 331 | game.physics() 332 | game.moveCamera() 333 | game.logic(tick) 334 | lastTick = newTick 335 | 336 | game.render() 337 | 338 | main() 339 | -------------------------------------------------------------------------------- /tutorial/platformer_part8.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#8.-text-rendering 2 | 3 | import 4 | sdl2, sdl2/image, sdl2/ttf, 5 | basic2d, strutils, times, math, strfmt 6 | 7 | type 8 | SDLException = object of Exception 9 | 10 | Input {.pure.} = enum none, left, right, jump, restart, quit 11 | 12 | Collision {.pure.} = enum x, y, corner 13 | 14 | Time = ref object 15 | begin, finish, best: int 16 | 17 | Player = ref object 18 | texture: TexturePtr 19 | pos: Point2d 20 | vel: Vector2d 21 | time: Time 22 | 23 | Map = ref object 24 | texture: TexturePtr 25 | width, height: int 26 | tiles: seq[uint8] 27 | 28 | Game = ref object 29 | inputs: array[Input, bool] 30 | renderer: RendererPtr 31 | font: FontPtr 32 | player: Player 33 | map: Map 34 | camera: Vector2d 35 | 36 | const 37 | windowSize: Point = (1280.cint, 720.cint) 38 | 39 | tilesPerRow = 16 40 | tileSize: Point = (64.cint, 64.cint) 41 | 42 | playerSize = vector2d(64, 64) 43 | 44 | air = 0 45 | start = 78 46 | finish = 110 47 | 48 | template sdlFailIf(cond: typed, reason: string) = 49 | if cond: raise SDLException.newException( 50 | reason & ", SDL error: " & $getError()) 51 | 52 | proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) = 53 | let 54 | x = pos.x.cint 55 | y = pos.y.cint 56 | 57 | var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [ 58 | (rect(192, 64, 64, 32), rect(x-60, y, 96, 48), 59 | SDL_FLIP_NONE), # back feet shadow 60 | (rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96), 61 | SDL_FLIP_NONE), # body shadow 62 | (rect(192, 64, 64, 32), rect(x-36, y, 96, 48), 63 | SDL_FLIP_NONE), # front feet shadow 64 | (rect(192, 32, 64, 32), rect(x-60, y, 96, 48), 65 | SDL_FLIP_NONE), # back feet 66 | (rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96), 67 | SDL_FLIP_NONE), # body 68 | (rect(192, 32, 64, 32), rect(x-36, y, 96, 48), 69 | SDL_FLIP_NONE), # front feet 70 | (rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36), 71 | SDL_FLIP_NONE), # left eye 72 | (rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36), 73 | SDL_FLIP_HORIZONTAL) # right eye 74 | ] 75 | 76 | for part in bodyParts.mitems: 77 | renderer.copyEx(texture, part.source, part.dest, angle = 0.0, 78 | center = nil, flip = part.flip) 79 | 80 | proc renderMap(renderer: RendererPtr, map: Map, camera: Vector2d) = 81 | var 82 | clip = rect(0, 0, tileSize.x, tileSize.y) 83 | dest = rect(0, 0, tileSize.x, tileSize.y) 84 | 85 | for i, tileNr in map.tiles: 86 | if tileNr == 0: continue 87 | 88 | clip.x = cint(tileNr mod tilesPerRow) * tileSize.x 89 | clip.y = cint(tileNr div tilesPerRow) * tileSize.y 90 | dest.x = cint(i mod map.width) * tileSize.x - camera.x.cint 91 | dest.y = cint(i div map.width) * tileSize.y - camera.y.cint 92 | 93 | renderer.copy(map.texture, unsafeAddr clip, unsafeAddr dest) 94 | 95 | proc renderText(renderer: RendererPtr, font: FontPtr, text: string, 96 | x, y, outline: cint, color: Color) = 97 | font.setFontOutline(outline) 98 | let surface = font.renderUtf8Blended(text.cstring, color) 99 | sdlFailIf surface.isNil: "Could not render text surface" 100 | 101 | discard surface.setSurfaceAlphaMod(color.a) 102 | 103 | var source = rect(0, 0, surface.w, surface.h) 104 | var dest = rect(x - outline, y - outline, surface.w, surface.h) 105 | let texture = renderer.createTextureFromSurface(surface) 106 | sdlFailIf texture.isNil: "Could not create texture from rendered text" 107 | 108 | surface.freeSurface() 109 | 110 | renderer.copyEx(texture, source, dest, angle = 0.0, center = nil, 111 | flip = SDL_FLIP_NONE) 112 | 113 | texture.destroy() 114 | 115 | proc renderText(game: Game, text: string, x, y: cint, color: Color) = 116 | const outlineColor = color(0, 0, 0, 64) 117 | game.renderer.renderText(game.font, text, x, y, outline = 2, outlineColor) 118 | game.renderer.renderText(game.font, text, x, y, outline = 0, color) 119 | 120 | proc restartPlayer(player: Player) = 121 | player.pos = point2d(170, 500) 122 | player.vel = vector2d(0, 0) 123 | player.time.begin = -1 124 | player.time.finish = -1 125 | 126 | proc newTime: Time = 127 | new result 128 | result.finish = -1 129 | result.best = -1 130 | 131 | proc newPlayer(texture: TexturePtr): Player = 132 | new result 133 | result.texture = texture 134 | result.time = newTime() 135 | result.restartPlayer() 136 | 137 | proc newMap(texture: TexturePtr, file: string): Map = 138 | new result 139 | result.texture = texture 140 | result.tiles = @[] 141 | 142 | for line in file.lines: 143 | var width = 0 144 | for word in line.split(' '): 145 | if word == "": continue 146 | let value = parseUInt(word) 147 | if value > uint(uint8.high): 148 | raise ValueError.newException( 149 | "Invalid value " & word & " in map " & file) 150 | result.tiles.add value.uint8 151 | inc width 152 | 153 | if result.width > 0 and result.width != width: 154 | raise ValueError.newException( 155 | "Incompatible line length in map " & file) 156 | result.width = width 157 | inc result.height 158 | 159 | proc newGame(renderer: RendererPtr): Game = 160 | new result 161 | result.renderer = renderer 162 | 163 | result.font = openFont("DejaVuSans.ttf", 28) 164 | sdlFailIf result.font.isNil: "Failed to load font" 165 | 166 | result.player = newPlayer(renderer.loadTexture("player.png")) 167 | result.map = newMap(renderer.loadTexture("grass.png"), "default.map") 168 | 169 | proc toInput(key: Scancode): Input = 170 | case key 171 | of SDL_SCANCODE_A: Input.left 172 | of SDL_SCANCODE_D: Input.right 173 | of SDL_SCANCODE_SPACE: Input.jump 174 | of SDL_SCANCODE_R: Input.restart 175 | of SDL_SCANCODE_Q: Input.quit 176 | else: Input.none 177 | 178 | proc handleInput(game: Game) = 179 | var event = defaultEvent 180 | while pollEvent(event): 181 | case event.kind 182 | of QuitEvent: 183 | game.inputs[Input.quit] = true 184 | of KeyDown: 185 | game.inputs[event.key.keysym.scancode.toInput] = true 186 | of KeyUp: 187 | game.inputs[event.key.keysym.scancode.toInput] = false 188 | else: 189 | discard 190 | 191 | proc formatTime(ticks: int): string = 192 | let 193 | mins = (ticks div 50) div 60 194 | secs = (ticks div 50) mod 60 195 | cents = (ticks mod 50) * 2 196 | interp"${mins:02}:${secs:02}:${cents:02}" 197 | 198 | proc render(game: Game, tick: int) = 199 | # Draw over all drawings of the last frame with the default color 200 | game.renderer.clear() 201 | # Actual drawing here 202 | game.renderer.renderTee(game.player.texture, game.player.pos - game.camera) 203 | game.renderer.renderMap(game.map, game.camera) 204 | 205 | let time = game.player.time 206 | const white = color(255, 255, 255, 255) 207 | if time.begin >= 0: 208 | game.renderText(formatTime(tick - time.begin), 50, 100, white) 209 | elif time.finish >= 0: 210 | game.renderText("Finished in: " & formatTime(time.finish), 50, 100, white) 211 | if time.best >= 0: 212 | game.renderText("Best time: " & formatTime(time.best), 50, 150, white) 213 | 214 | # Show the result on screen 215 | game.renderer.present() 216 | 217 | proc getTile(map: Map, x, y: int): uint8 = 218 | let 219 | nx = clamp(x div tileSize.x, 0, map.width - 1) 220 | ny = clamp(y div tileSize.y, 0, map.height - 1) 221 | pos = ny * map.width + nx 222 | 223 | map.tiles[pos] 224 | 225 | proc getTile(map: Map, pos: Point2d): uint8 = 226 | map.getTile(pos.x.round.int, pos.y.round.int) 227 | 228 | proc isSolid(map: Map, x, y: int): bool = 229 | map.getTile(x, y) notin {air, start, finish} 230 | 231 | proc isSolid(map: Map, point: Point2d): bool = 232 | map.isSolid(point.x.round.int, point.y.round.int) 233 | 234 | proc onGround(map: Map, pos: Point2d, size: Vector2d): bool = 235 | let size = size * 0.5 236 | result = 237 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y + 1)) or 238 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y + 1)) 239 | 240 | proc testBox(map: Map, pos: Point2d, size: Vector2d): bool = 241 | let size = size * 0.5 242 | result = 243 | map.isSolid(point2d(pos.x - size.x, pos.y - size.y)) or 244 | map.isSolid(point2d(pos.x + size.x, pos.y - size.y)) or 245 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y)) or 246 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y)) 247 | 248 | proc moveBox(map: Map, pos: var Point2d, vel: var Vector2d, 249 | size: Vector2d): set[Collision] {.discardable.} = 250 | let distance = vel.len 251 | let maximum = distance.int 252 | 253 | if distance < 0: 254 | return 255 | 256 | let fraction = 1.0 / float(maximum + 1) 257 | 258 | for i in 0 .. maximum: 259 | var newPos = pos + vel * fraction 260 | 261 | if map.testBox(newPos, size): 262 | var hit = false 263 | 264 | if map.testBox(point2d(pos.x, newPos.y), size): 265 | result.incl Collision.y 266 | newPos.y = pos.y 267 | vel.y = 0 268 | hit = true 269 | 270 | if map.testBox(point2d(newPos.x, pos.y), size): 271 | result.incl Collision.x 272 | newPos.x = pos.x 273 | vel.x = 0 274 | hit = true 275 | 276 | if not hit: 277 | result.incl Collision.corner 278 | newPos = pos 279 | vel = vector2d(0, 0) 280 | 281 | pos = newPos 282 | 283 | proc physics(game: Game) = 284 | if game.inputs[Input.restart]: 285 | game.player.restartPlayer() 286 | 287 | let ground = game.map.onGround(game.player.pos, playerSize) 288 | 289 | if game.inputs[Input.jump]: 290 | if ground: 291 | game.player.vel.y = -21 292 | 293 | let direction = float(game.inputs[Input.right].int - 294 | game.inputs[Input.left].int) 295 | 296 | game.player.vel.y += 0.75 297 | if ground: 298 | game.player.vel.x = 0.5 * game.player.vel.x + 4.0 * direction 299 | else: 300 | game.player.vel.x = 0.95 * game.player.vel.x + 2.0 * direction 301 | game.player.vel.x = clamp(game.player.vel.x, -8, 8) 302 | 303 | game.map.moveBox(game.player.pos, game.player.vel, playerSize) 304 | 305 | proc moveCamera(game: Game) = 306 | const halfWin = float(windowSize.x div 2) 307 | when defined(fluidCamera): 308 | let dist = game.camera.x - game.player.pos.x + halfWin 309 | game.camera.x -= 0.05 * dist 310 | elif defined(innerCamera): 311 | let 312 | leftArea = game.player.pos.x - halfWin - 100 313 | rightArea = game.player.pos.x - halfWin + 100 314 | game.camera.x = clamp(game.camera.x, leftArea, rightArea) 315 | else: 316 | game.camera.x = game.player.pos.x - halfWin 317 | 318 | proc logic(game: Game, tick: int) = 319 | template time: untyped = game.player.time 320 | case game.map.getTile(game.player.pos) 321 | of start: 322 | time.begin = tick 323 | of finish: 324 | if time.begin >= 0: 325 | time.finish = tick - time.begin 326 | time.begin = -1 327 | if time.best < 0 or time.finish < time.best: 328 | time.best = time.finish 329 | else: discard 330 | 331 | proc main = 332 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 333 | "SDL2 initialization failed" 334 | 335 | # defer blocks get called at the end of the procedure, even if an 336 | # exception has been thrown 337 | defer: sdl2.quit() 338 | 339 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 340 | "Linear texture filtering could not be enabled" 341 | 342 | const imgFlags: cint = IMG_INIT_PNG 343 | sdlFailIf(image.init(imgFlags) != imgFlags): 344 | "SDL2 Image initialization failed" 345 | defer: image.quit() 346 | 347 | sdlFailIf(ttfInit() == SdlError): "SDL2 TTF initialization failed" 348 | defer: ttfQuit() 349 | 350 | let window = createWindow(title = "Our own 2D platformer", 351 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 352 | w = windowSize.x, h = windowSize.y, flags = SDL_WINDOW_SHOWN) 353 | sdlFailIf window.isNil: "Window could not be created" 354 | defer: window.destroy() 355 | 356 | let renderer = window.createRenderer(index = -1, 357 | flags = Renderer_Accelerated or Renderer_PresentVsync) 358 | sdlFailIf renderer.isNil: "Renderer could not be created" 359 | defer: renderer.destroy() 360 | 361 | # Set the default color to use for drawing 362 | renderer.setDrawColor(r = 110, g = 132, b = 174) 363 | 364 | var 365 | game = newGame(renderer) 366 | startTime = epochTime() 367 | lastTick = 0 368 | 369 | # Game loop, draws each frame 370 | while not game.inputs[Input.quit]: 371 | game.handleInput() 372 | 373 | let newTick = int((epochTime() - startTime) * 50) 374 | for tick in lastTick+1 .. newTick: 375 | game.physics() 376 | game.moveCamera() 377 | game.logic(tick) 378 | lastTick = newTick 379 | 380 | game.render(lastTick) 381 | 382 | main() 383 | -------------------------------------------------------------------------------- /tutorial/platformer_part9.nim: -------------------------------------------------------------------------------- 1 | # https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/#9.-text-caching 2 | 3 | import 4 | sdl2, sdl2/image, sdl2/ttf, 5 | basic2d, strutils, times, math, strfmt 6 | 7 | type 8 | SDLException = object of Exception 9 | 10 | Input {.pure.} = enum none, left, right, jump, restart, quit 11 | 12 | Collision {.pure.} = enum x, y, corner 13 | 14 | CacheLine = object 15 | texture: TexturePtr 16 | w, h: cint 17 | 18 | TextCache = ref object 19 | text: string 20 | cache: array[2, CacheLine] 21 | 22 | Time = ref object 23 | begin, finish, best: int 24 | 25 | Player = ref object 26 | texture: TexturePtr 27 | pos: Point2d 28 | vel: Vector2d 29 | time: Time 30 | 31 | Map = ref object 32 | texture: TexturePtr 33 | width, height: int 34 | tiles: seq[uint8] 35 | 36 | Game = ref object 37 | inputs: array[Input, bool] 38 | renderer: RendererPtr 39 | font: FontPtr 40 | player: Player 41 | map: Map 42 | camera: Vector2d 43 | 44 | const 45 | windowSize: Point = (1280.cint, 720.cint) 46 | 47 | tilesPerRow = 16 48 | tileSize: Point = (64.cint, 64.cint) 49 | 50 | playerSize = vector2d(64, 64) 51 | 52 | air = 0 53 | start = 78 54 | finish = 110 55 | 56 | template sdlFailIf(cond: typed, reason: string) = 57 | if cond: raise SDLException.newException( 58 | reason & ", SDL error: " & $getError()) 59 | 60 | proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) = 61 | let 62 | x = pos.x.cint 63 | y = pos.y.cint 64 | 65 | var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [ 66 | (rect(192, 64, 64, 32), rect(x-60, y, 96, 48), 67 | SDL_FLIP_NONE), # back feet shadow 68 | (rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96), 69 | SDL_FLIP_NONE), # body shadow 70 | (rect(192, 64, 64, 32), rect(x-36, y, 96, 48), 71 | SDL_FLIP_NONE), # front feet shadow 72 | (rect(192, 32, 64, 32), rect(x-60, y, 96, 48), 73 | SDL_FLIP_NONE), # back feet 74 | (rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96), 75 | SDL_FLIP_NONE), # body 76 | (rect(192, 32, 64, 32), rect(x-36, y, 96, 48), 77 | SDL_FLIP_NONE), # front feet 78 | (rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36), 79 | SDL_FLIP_NONE), # left eye 80 | (rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36), 81 | SDL_FLIP_HORIZONTAL) # right eye 82 | ] 83 | 84 | for part in bodyParts.mitems: 85 | renderer.copyEx(texture, part.source, part.dest, angle = 0.0, 86 | center = nil, flip = part.flip) 87 | 88 | proc renderMap(renderer: RendererPtr, map: Map, camera: Vector2d) = 89 | var 90 | clip = rect(0, 0, tileSize.x, tileSize.y) 91 | dest = rect(0, 0, tileSize.x, tileSize.y) 92 | 93 | for i, tileNr in map.tiles: 94 | if tileNr == 0: continue 95 | 96 | clip.x = cint(tileNr mod tilesPerRow) * tileSize.x 97 | clip.y = cint(tileNr div tilesPerRow) * tileSize.y 98 | dest.x = cint(i mod map.width) * tileSize.x - camera.x.cint 99 | dest.y = cint(i div map.width) * tileSize.y - camera.y.cint 100 | 101 | renderer.copy(map.texture, unsafeAddr clip, unsafeAddr dest) 102 | 103 | proc newTextCache: TextCache = 104 | new result 105 | 106 | proc renderText(renderer: RendererPtr, font: FontPtr, text: string, 107 | x, y, outline: cint, color: Color): CacheLine = 108 | font.setFontOutline(outline) 109 | let surface = font.renderUtf8Blended(text.cstring, color) 110 | sdlFailIf surface.isNil: "Could not render text surface" 111 | 112 | discard surface.setSurfaceAlphaMod(color.a) 113 | 114 | result.w = surface.w 115 | result.h = surface.h 116 | result.texture = renderer.createTextureFromSurface(surface) 117 | sdlFailIf result.texture.isNil: "Could not create texture from rendered text" 118 | 119 | surface.freeSurface() 120 | 121 | proc renderText(game: Game, text: string, x, y: cint, color: Color, 122 | tc: TextCache) = 123 | let passes = [(color: color(0, 0, 0, 64), outline: 2.cint), 124 | (color: color, outline: 0.cint)] 125 | 126 | if text != tc.text: 127 | for i in 0..1: 128 | tc.cache[i].texture.destroy() 129 | tc.cache[i] = game.renderer.renderText( 130 | game.font, text, x, y, passes[i].outline, passes[i].color) 131 | tc.text = text 132 | 133 | for i in 0..1: 134 | var source = rect(0, 0, tc.cache[i].w, tc.cache[i].h) 135 | var dest = rect(x - passes[i].outline, y - passes[i].outline, 136 | tc.cache[i].w, tc.cache[i].h) 137 | game.renderer.copyEx(tc.cache[i].texture, source, dest, 138 | angle = 0.0, center = nil) 139 | 140 | template renderTextCached(game: Game, text: string, x, y: cint, color: Color) = 141 | block: 142 | var tc {.global.} = newTextCache() 143 | game.renderText(text, x, y, color, tc) 144 | 145 | proc restartPlayer(player: Player) = 146 | player.pos = point2d(170, 500) 147 | player.vel = vector2d(0, 0) 148 | player.time.begin = -1 149 | player.time.finish = -1 150 | 151 | proc newTime: Time = 152 | new result 153 | result.finish = -1 154 | result.best = -1 155 | 156 | proc newPlayer(texture: TexturePtr): Player = 157 | new result 158 | result.texture = texture 159 | result.time = newTime() 160 | result.restartPlayer() 161 | 162 | proc newMap(texture: TexturePtr, file: string): Map = 163 | new result 164 | result.texture = texture 165 | result.tiles = @[] 166 | 167 | for line in file.lines: 168 | var width = 0 169 | for word in line.split(' '): 170 | if word == "": continue 171 | let value = parseUInt(word) 172 | if value > uint(uint8.high): 173 | raise ValueError.newException( 174 | "Invalid value " & word & " in map " & file) 175 | result.tiles.add value.uint8 176 | inc width 177 | 178 | if result.width > 0 and result.width != width: 179 | raise ValueError.newException( 180 | "Incompatible line length in map " & file) 181 | result.width = width 182 | inc result.height 183 | 184 | proc newGame(renderer: RendererPtr): Game = 185 | new result 186 | result.renderer = renderer 187 | 188 | result.font = openFont("DejaVuSans.ttf", 28) 189 | sdlFailIf result.font.isNil: "Failed to load font" 190 | 191 | result.player = newPlayer(renderer.loadTexture("player.png")) 192 | result.map = newMap(renderer.loadTexture("grass.png"), "default.map") 193 | 194 | proc toInput(key: Scancode): Input = 195 | case key 196 | of SDL_SCANCODE_A: Input.left 197 | of SDL_SCANCODE_D: Input.right 198 | of SDL_SCANCODE_SPACE: Input.jump 199 | of SDL_SCANCODE_R: Input.restart 200 | of SDL_SCANCODE_Q: Input.quit 201 | else: Input.none 202 | 203 | proc handleInput(game: Game) = 204 | var event = defaultEvent 205 | while pollEvent(event): 206 | case event.kind 207 | of QuitEvent: 208 | game.inputs[Input.quit] = true 209 | of KeyDown: 210 | game.inputs[event.key.keysym.scancode.toInput] = true 211 | of KeyUp: 212 | game.inputs[event.key.keysym.scancode.toInput] = false 213 | else: 214 | discard 215 | 216 | proc formatTime(ticks: int): string = 217 | let mins = (ticks div 50) div 60 218 | let secs = (ticks div 50) mod 60 219 | interp"${mins:02}:${secs:02}" 220 | 221 | proc formatTimeExact(ticks: int): string = 222 | let cents = (ticks mod 50) * 2 223 | interp"${formatTime(ticks)}:${cents:02}" 224 | 225 | proc render(game: Game, tick: int) = 226 | # Draw over all drawings of the last frame with the default color 227 | game.renderer.clear() 228 | # Actual drawing here 229 | game.renderer.renderTee(game.player.texture, game.player.pos - game.camera) 230 | game.renderer.renderMap(game.map, game.camera) 231 | 232 | let time = game.player.time 233 | const white = color(255, 255, 255, 255) 234 | if time.begin >= 0: 235 | game.renderTextCached(formatTime(tick - time.begin), 50, 100, white) 236 | elif time.finish >= 0: 237 | game.renderTextCached("Finished in: " & formatTimeExact(time.finish), 238 | 50, 100, white) 239 | if time.best >= 0: 240 | game.renderTextCached("Best time: " & formatTimeExact(time.best), 241 | 50, 150, white) 242 | 243 | # Show the result on screen 244 | game.renderer.present() 245 | 246 | proc getTile(map: Map, x, y: int): uint8 = 247 | let 248 | nx = clamp(x div tileSize.x, 0, map.width - 1) 249 | ny = clamp(y div tileSize.y, 0, map.height - 1) 250 | pos = ny * map.width + nx 251 | 252 | map.tiles[pos] 253 | 254 | proc getTile(map: Map, pos: Point2d): uint8 = 255 | map.getTile(pos.x.round.int, pos.y.round.int) 256 | 257 | proc isSolid(map: Map, x, y: int): bool = 258 | map.getTile(x, y) notin {air, start, finish} 259 | 260 | proc isSolid(map: Map, point: Point2d): bool = 261 | map.isSolid(point.x.round.int, point.y.round.int) 262 | 263 | proc onGround(map: Map, pos: Point2d, size: Vector2d): bool = 264 | let size = size * 0.5 265 | result = 266 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y + 1)) or 267 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y + 1)) 268 | 269 | proc testBox(map: Map, pos: Point2d, size: Vector2d): bool = 270 | let size = size * 0.5 271 | result = 272 | map.isSolid(point2d(pos.x - size.x, pos.y - size.y)) or 273 | map.isSolid(point2d(pos.x + size.x, pos.y - size.y)) or 274 | map.isSolid(point2d(pos.x - size.x, pos.y + size.y)) or 275 | map.isSolid(point2d(pos.x + size.x, pos.y + size.y)) 276 | 277 | proc moveBox(map: Map, pos: var Point2d, vel: var Vector2d, 278 | size: Vector2d): set[Collision] {.discardable.} = 279 | let distance = vel.len 280 | let maximum = distance.int 281 | 282 | if distance < 0: 283 | return 284 | 285 | let fraction = 1.0 / float(maximum + 1) 286 | 287 | for i in 0 .. maximum: 288 | var newPos = pos + vel * fraction 289 | 290 | if map.testBox(newPos, size): 291 | var hit = false 292 | 293 | if map.testBox(point2d(pos.x, newPos.y), size): 294 | result.incl Collision.y 295 | newPos.y = pos.y 296 | vel.y = 0 297 | hit = true 298 | 299 | if map.testBox(point2d(newPos.x, pos.y), size): 300 | result.incl Collision.x 301 | newPos.x = pos.x 302 | vel.x = 0 303 | hit = true 304 | 305 | if not hit: 306 | result.incl Collision.corner 307 | newPos = pos 308 | vel = vector2d(0, 0) 309 | 310 | pos = newPos 311 | 312 | proc physics(game: Game) = 313 | if game.inputs[Input.restart]: 314 | game.player.restartPlayer() 315 | 316 | let ground = game.map.onGround(game.player.pos, playerSize) 317 | 318 | if game.inputs[Input.jump]: 319 | if ground: 320 | game.player.vel.y = -21 321 | 322 | let direction = float(game.inputs[Input.right].int - 323 | game.inputs[Input.left].int) 324 | 325 | game.player.vel.y += 0.75 326 | if ground: 327 | game.player.vel.x = 0.5 * game.player.vel.x + 4.0 * direction 328 | else: 329 | game.player.vel.x = 0.95 * game.player.vel.x + 2.0 * direction 330 | game.player.vel.x = clamp(game.player.vel.x, -8, 8) 331 | 332 | game.map.moveBox(game.player.pos, game.player.vel, playerSize) 333 | 334 | proc moveCamera(game: Game) = 335 | const halfWin = float(windowSize.x div 2) 336 | when defined(fluidCamera): 337 | let dist = game.camera.x - game.player.pos.x + halfWin 338 | game.camera.x -= 0.05 * dist 339 | elif defined(innerCamera): 340 | let 341 | leftArea = game.player.pos.x - halfWin - 100 342 | rightArea = game.player.pos.x - halfWin + 100 343 | game.camera.x = clamp(game.camera.x, leftArea, rightArea) 344 | else: 345 | game.camera.x = game.player.pos.x - halfWin 346 | 347 | proc logic(game: Game, tick: int) = 348 | template time: untyped = game.player.time 349 | case game.map.getTile(game.player.pos) 350 | of start: 351 | time.begin = tick 352 | of finish: 353 | if time.begin >= 0: 354 | time.finish = tick - time.begin 355 | time.begin = -1 356 | if time.best < 0 or time.finish < time.best: 357 | time.best = time.finish 358 | else: discard 359 | 360 | proc main = 361 | sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)): 362 | "SDL2 initialization failed" 363 | 364 | # defer blocks get called at the end of the procedure, even if an 365 | # exception has been thrown 366 | defer: sdl2.quit() 367 | 368 | sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")): 369 | "Linear texture filtering could not be enabled" 370 | 371 | const imgFlags: cint = IMG_INIT_PNG 372 | sdlFailIf(image.init(imgFlags) != imgFlags): 373 | "SDL2 Image initialization failed" 374 | defer: image.quit() 375 | 376 | sdlFailIf(ttfInit() == SdlError): "SDL2 TTF initialization failed" 377 | defer: ttfQuit() 378 | 379 | let window = createWindow(title = "Our own 2D platformer", 380 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 381 | w = windowSize.x, h = windowSize.y, flags = SDL_WINDOW_SHOWN) 382 | sdlFailIf window.isNil: "Window could not be created" 383 | defer: window.destroy() 384 | 385 | let renderer = window.createRenderer(index = -1, 386 | flags = Renderer_Accelerated or Renderer_PresentVsync) 387 | sdlFailIf renderer.isNil: "Renderer could not be created" 388 | defer: renderer.destroy() 389 | 390 | # Set the default color to use for drawing 391 | renderer.setDrawColor(r = 110, g = 132, b = 174) 392 | 393 | var 394 | game = newGame(renderer) 395 | startTime = epochTime() 396 | lastTick = 0 397 | 398 | # Game loop, draws each frame 399 | while not game.inputs[Input.quit]: 400 | game.handleInput() 401 | 402 | let newTick = int((epochTime() - startTime) * 50) 403 | for tick in lastTick+1 .. newTick: 404 | game.physics() 405 | game.moveCamera() 406 | game.logic(tick) 407 | lastTick = newTick 408 | 409 | game.render(lastTick) 410 | 411 | main() 412 | -------------------------------------------------------------------------------- /tutorial/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/def-/nim-platformer/9e79719369ae1722c01b4546a54792a77413c22f/tutorial/player.png --------------------------------------------------------------------------------