forked from def-/nim-platformer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplatformer.nim
440 lines (352 loc) · 12.5 KB
/
platformer.nim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
import
sdl2, sdl2/image, sdl2/ttf,
basic2d, strutils, times, math, strfmt, os, streams
type
SDLException = object of Exception
Input {.pure.} = enum none, left, right, jump, restart, quit
Collision {.pure.} = enum x, y, corner
CacheLine = object
texture: TexturePtr
w, h: cint
TextCache = ref object
text: string
cache: array[2, CacheLine]
Time = ref object
begin, finish, best: int
Player = ref object
texture: TexturePtr
pos: Point2d
vel: Vector2d
time: Time
Map = ref object
texture: TexturePtr
width, height: int
tiles: seq[uint8]
Game = ref object
inputs: array[Input, bool]
renderer: RendererPtr
font: FontPtr
player: Player
map: Map
camera: Vector2d
const
windowSize: Point = (1280.cint, 720.cint)
tilesPerRow = 16
tileSize: Point = (64.cint, 64.cint)
playerSize = vector2d(64, 64)
air = 0
start = 78
finish = 110
template sdlFailIf(cond: typed, reason: string) =
if cond: raise SDLException.newException(
reason & ", SDL error: " & $getError())
proc renderTee(renderer: RendererPtr, texture: TexturePtr, pos: Point2d) =
let
x = pos.x.cint
y = pos.y.cint
var bodyParts: array[8, tuple[source, dest: Rect, flip: cint]] = [
(rect(192, 64, 64, 32), rect(x-60, y, 96, 48),
SDL_FLIP_NONE), # back feet shadow
(rect( 96, 0, 96, 96), rect(x-48, y-48, 96, 96),
SDL_FLIP_NONE), # body shadow
(rect(192, 64, 64, 32), rect(x-36, y, 96, 48),
SDL_FLIP_NONE), # front feet shadow
(rect(192, 32, 64, 32), rect(x-60, y, 96, 48),
SDL_FLIP_NONE), # back feet
(rect( 0, 0, 96, 96), rect(x-48, y-48, 96, 96),
SDL_FLIP_NONE), # body
(rect(192, 32, 64, 32), rect(x-36, y, 96, 48),
SDL_FLIP_NONE), # front feet
(rect( 64, 96, 32, 32), rect(x-18, y-21, 36, 36),
SDL_FLIP_NONE), # left eye
(rect( 64, 96, 32, 32), rect( x-6, y-21, 36, 36),
SDL_FLIP_HORIZONTAL) # right eye
]
for part in bodyParts.mitems:
renderer.copyEx(texture, part.source, part.dest, angle = 0.0,
center = nil, flip = part.flip)
proc renderMap(renderer: RendererPtr, map: Map, camera: Vector2d) =
var
clip = rect(0, 0, tileSize.x, tileSize.y)
dest = rect(0, 0, tileSize.x, tileSize.y)
for i, tileNr in map.tiles:
if tileNr == 0: continue
clip.x = cint(tileNr mod tilesPerRow) * tileSize.x
clip.y = cint(tileNr div tilesPerRow) * tileSize.y
dest.x = cint(i mod map.width) * tileSize.x - camera.x.cint
dest.y = cint(i div map.width) * tileSize.y - camera.y.cint
renderer.copy(map.texture, unsafeAddr clip, unsafeAddr dest)
proc newTextCache: TextCache =
new result
proc renderText(renderer: RendererPtr, font: FontPtr, text: string,
x, y, outline: cint, color: Color): CacheLine =
font.setFontOutline(outline)
let surface = font.renderUtf8Blended(text.cstring, color)
sdlFailIf surface.isNil: "Could not render text surface"
discard surface.setSurfaceAlphaMod(color.a)
result.w = surface.w
result.h = surface.h
result.texture = renderer.createTextureFromSurface(surface)
sdlFailIf result.texture.isNil: "Could not create texture from rendered text"
surface.freeSurface()
proc renderText(game: Game, text: string, x, y: cint, color: Color,
tc: TextCache) =
let passes = [(color: color(0, 0, 0, 64), outline: 2.cint),
(color: color, outline: 0.cint)]
if text != tc.text:
for i in 0..1:
tc.cache[i].texture.destroy()
tc.cache[i] = game.renderer.renderText(
game.font, text, x, y, passes[i].outline, passes[i].color)
tc.text = text
for i in 0..1:
var source = rect(0, 0, tc.cache[i].w, tc.cache[i].h)
var dest = rect(x - passes[i].outline, y - passes[i].outline,
tc.cache[i].w, tc.cache[i].h)
game.renderer.copyEx(tc.cache[i].texture, source, dest,
angle = 0.0, center = nil)
template renderTextCached(game: Game, text: string, x, y: cint, color: Color) =
block:
var tc {.global.} = newTextCache()
game.renderText(text, x, y, color, tc)
proc restartPlayer(player: Player) =
player.pos = point2d(170, 500)
player.vel = vector2d(0, 0)
player.time.begin = -1
player.time.finish = -1
proc newTime: Time =
new result
result.finish = -1
result.best = -1
proc newPlayer(texture: TexturePtr): Player =
new result
result.texture = texture
result.time = newTime()
result.restartPlayer()
proc newMap(texture: TexturePtr, map: Stream): Map =
new result
result.texture = texture
result.tiles = @[]
var line = ""
while map.readLine(line):
var width = 0
for word in line.split(' '):
if word == "": continue
let value = parseUInt(word)
if value > uint(uint8.high):
raise ValueError.newException(
"Invalid value in map: " & word)
result.tiles.add value.uint8
inc width
if width == 0: continue
if result.width > 0 and result.width != width:
raise ValueError.newException(
"Incompatible line length in map: " & $width)
result.width = width
inc result.height
const dataDir = "data"
when defined(embedData):
template readRW(filename: string): ptr RWops =
const file = staticRead(dataDir / filename)
rwFromConstMem(file.cstring, file.len)
template readStream(filename: string): Stream =
const file = staticRead(dataDir / filename)
newStringStream(file)
else:
let fullDataDir = getAppDir() / dataDir
template readRW(filename: string): ptr RWops =
var rw = rwFromFile(cstring(fullDataDir / filename), "r")
sdlFailIf rw.isNil: "Cannot create RWops from file"
rw
template readStream(filename: string): Stream =
var stream = newFileStream(fullDataDir / filename)
if stream.isNil: raise ValueError.newException(
"Cannot open file stream:" & fullDataDir / filename)
stream
proc newGame(renderer: RendererPtr): Game =
new result
result.renderer = renderer
result.font = openFontRW(
readRW("DejaVuSans.ttf"), freesrc = 1, 28)
sdlFailIf result.font.isNil: "Failed to load font"
result.player = newPlayer(renderer.loadTexture_RW(
readRW("player.png"), freesrc = 1))
result.map = newMap(renderer.loadTexture_RW(
readRW("grass.png"), freesrc = 1),
readStream("default.map"))
proc toInput(key: Scancode): Input =
case key
of SDL_SCANCODE_A: Input.left
of SDL_SCANCODE_D: Input.right
of SDL_SCANCODE_SPACE: Input.jump
of SDL_SCANCODE_R: Input.restart
of SDL_SCANCODE_Q: Input.quit
else: Input.none
proc handleInput(game: Game) =
var event = defaultEvent
while pollEvent(event):
case event.kind
of QuitEvent:
game.inputs[Input.quit] = true
of KeyDown:
game.inputs[event.key.keysym.scancode.toInput] = true
of KeyUp:
game.inputs[event.key.keysym.scancode.toInput] = false
else:
discard
proc formatTime(ticks: int): string =
let mins = (ticks div 50) div 60
let secs = (ticks div 50) mod 60
interp"${mins:02}:${secs:02}"
proc formatTimeExact(ticks: int): string =
let cents = (ticks mod 50) * 2
interp"${formatTime(ticks)}:${cents:02}"
proc render(game: Game, tick: int) =
# Draw over all drawings of the last frame with the default color
game.renderer.clear()
# Actual drawing here
game.renderer.renderTee(game.player.texture, game.player.pos - game.camera)
game.renderer.renderMap(game.map, game.camera)
let time = game.player.time
const white = color(255, 255, 255, 255)
if time.begin >= 0:
game.renderTextCached(formatTime(tick - time.begin), 50, 100, white)
elif time.finish >= 0:
game.renderTextCached("Finished in: " & formatTimeExact(time.finish),
50, 100, white)
if time.best >= 0:
game.renderTextCached("Best time: " & formatTimeExact(time.best),
50, 150, white)
# Show the result on screen
game.renderer.present()
proc getTile(map: Map, x, y: int): uint8 =
let
nx = clamp(x div tileSize.x, 0, map.width - 1)
ny = clamp(y div tileSize.y, 0, map.height - 1)
pos = ny * map.width + nx
map.tiles[pos]
proc getTile(map: Map, pos: Point2d): uint8 =
map.getTile(pos.x.round.int, pos.y.round.int)
proc isSolid(map: Map, x, y: int): bool =
map.getTile(x, y) notin {air, start, finish}
proc isSolid(map: Map, point: Point2d): bool =
map.isSolid(point.x.round.int, point.y.round.int)
proc onGround(map: Map, pos: Point2d, size: Vector2d): bool =
let size = size * 0.5
result =
map.isSolid(point2d(pos.x - size.x, pos.y + size.y + 1)) or
map.isSolid(point2d(pos.x + size.x, pos.y + size.y + 1))
proc testBox(map: Map, pos: Point2d, size: Vector2d): bool =
let size = size * 0.5
result =
map.isSolid(point2d(pos.x - size.x, pos.y - size.y)) or
map.isSolid(point2d(pos.x + size.x, pos.y - size.y)) or
map.isSolid(point2d(pos.x - size.x, pos.y + size.y)) or
map.isSolid(point2d(pos.x + size.x, pos.y + size.y))
proc moveBox(map: Map, pos: var Point2d, vel: var Vector2d,
size: Vector2d): set[Collision] {.discardable.} =
let distance = vel.len
let maximum = distance.int
if distance < 0:
return
let fraction = 1.0 / float(maximum + 1)
for i in 0 .. maximum:
var newPos = pos + vel * fraction
if map.testBox(newPos, size):
var hit = false
if map.testBox(point2d(pos.x, newPos.y), size):
result.incl Collision.y
newPos.y = pos.y
vel.y = 0
hit = true
if map.testBox(point2d(newPos.x, pos.y), size):
result.incl Collision.x
newPos.x = pos.x
vel.x = 0
hit = true
if not hit:
result.incl Collision.corner
newPos = pos
vel = vector2d(0, 0)
pos = newPos
proc physics(game: Game) =
if game.inputs[Input.restart]:
game.player.restartPlayer()
let ground = game.map.onGround(game.player.pos, playerSize)
if game.inputs[Input.jump]:
if ground:
game.player.vel.y = -21
let direction = float(game.inputs[Input.right].int -
game.inputs[Input.left].int)
game.player.vel.y += 0.75
if ground:
game.player.vel.x = 0.5 * game.player.vel.x + 4.0 * direction
else:
game.player.vel.x = 0.95 * game.player.vel.x + 2.0 * direction
game.player.vel.x = clamp(game.player.vel.x, -8, 8)
game.map.moveBox(game.player.pos, game.player.vel, playerSize)
proc moveCamera(game: Game) =
const halfWin = float(windowSize.x div 2)
when defined(fluidCamera):
let dist = game.camera.x - game.player.pos.x + halfWin
game.camera.x -= 0.05 * dist
elif defined(innerCamera):
let
leftArea = game.player.pos.x - halfWin - 100
rightArea = game.player.pos.x - halfWin + 100
game.camera.x = clamp(game.camera.x, leftArea, rightArea)
else:
game.camera.x = game.player.pos.x - halfWin
proc logic(game: Game, tick: int) =
template time: untyped = game.player.time
case game.map.getTile(game.player.pos)
of start:
time.begin = tick
of finish:
if time.begin >= 0:
time.finish = tick - time.begin
time.begin = -1
if time.best < 0 or time.finish < time.best:
time.best = time.finish
else: discard
proc main =
sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)):
"SDL2 initialization failed"
# defer blocks get called at the end of the procedure, even if an
# exception has been thrown
defer: sdl2.quit()
sdlFailIf(not setHint("SDL_RENDER_SCALE_QUALITY", "2")):
"Linear texture filtering could not be enabled"
const imgFlags: cint = IMG_INIT_PNG
sdlFailIf(image.init(imgFlags) != imgFlags):
"SDL2 Image initialization failed"
defer: image.quit()
sdlFailIf(ttfInit() == SdlError):
"SDL2 TTF initialization failed"
defer: ttfQuit()
let window = createWindow(title = "Our own 2D platformer",
x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED,
w = windowSize.x, h = windowSize.y, flags = SDL_WINDOW_SHOWN)
sdlFailIf window.isNil: "Window could not be created"
defer: window.destroy()
let renderer = window.createRenderer(index = -1,
flags = Renderer_Accelerated or Renderer_PresentVsync)
sdlFailIf renderer.isNil: "Renderer could not be created"
defer: renderer.destroy()
# Set the default color to use for drawing
renderer.setDrawColor(r = 110, g = 132, b = 174)
var
game = newGame(renderer)
startTime = epochTime()
lastTick = 0
# Game loop, draws each frame
while not game.inputs[Input.quit]:
game.handleInput()
let newTick = int((epochTime() - startTime) * 50)
for tick in lastTick+1 .. newTick:
game.physics()
game.moveCamera()
game.logic(tick)
lastTick = newTick
game.render(lastTick)
main()