├── Wolf3D.pdx ├── main.pdz ├── Images │ ├── hands.pdt │ ├── wall_tiles.pdt │ ├── minimap_mask.pdi │ ├── background_flat.pdi │ ├── minimap_overlay.pdi │ ├── background_gradient.pdi │ └── minimap_overlay_v2.pdi ├── SFX │ └── gun-shot.pda ├── SystemAssets │ ├── card.pdi │ └── launchImage.pdi └── pdxinfo ├── Source ├── SFX │ └── gun-shot.wav ├── SystemAssets │ ├── card.png │ └── launchImage.png ├── Images │ ├── minimap_mask.png │ ├── background_flat.png │ ├── minimap_overlay.png │ ├── background_gradient.png │ ├── hands-table-176-160.png │ ├── minimap_overlay_v2.png │ └── wall_tiles-table-16-16.png ├── pdxinfo └── main.lua ├── .gitignore ├── makefile ├── LICENSE ├── README.md └── 220818_main_backup.lua /Wolf3D.pdx/main.pdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/main.pdz -------------------------------------------------------------------------------- /Source/SFX/gun-shot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/SFX/gun-shot.wav -------------------------------------------------------------------------------- /Wolf3D.pdx/Images/hands.pdt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/Images/hands.pdt -------------------------------------------------------------------------------- /Wolf3D.pdx/SFX/gun-shot.pda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/SFX/gun-shot.pda -------------------------------------------------------------------------------- /Source/SystemAssets/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/SystemAssets/card.png -------------------------------------------------------------------------------- /Source/Images/minimap_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/Images/minimap_mask.png -------------------------------------------------------------------------------- /Wolf3D.pdx/Images/wall_tiles.pdt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/Images/wall_tiles.pdt -------------------------------------------------------------------------------- /Wolf3D.pdx/SystemAssets/card.pdi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/SystemAssets/card.pdi -------------------------------------------------------------------------------- /Source/Images/background_flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/Images/background_flat.png -------------------------------------------------------------------------------- /Source/Images/minimap_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/Images/minimap_overlay.png -------------------------------------------------------------------------------- /Wolf3D.pdx/Images/minimap_mask.pdi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/Images/minimap_mask.pdi -------------------------------------------------------------------------------- /Source/Images/background_gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/Images/background_gradient.png -------------------------------------------------------------------------------- /Source/Images/hands-table-176-160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/Images/hands-table-176-160.png -------------------------------------------------------------------------------- /Source/Images/minimap_overlay_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/Images/minimap_overlay_v2.png -------------------------------------------------------------------------------- /Source/SystemAssets/launchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/SystemAssets/launchImage.png -------------------------------------------------------------------------------- /Wolf3D.pdx/Images/background_flat.pdi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/Images/background_flat.pdi -------------------------------------------------------------------------------- /Wolf3D.pdx/Images/minimap_overlay.pdi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/Images/minimap_overlay.pdi -------------------------------------------------------------------------------- /Wolf3D.pdx/SystemAssets/launchImage.pdi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/SystemAssets/launchImage.pdi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .AppleDouble 3 | .LSOverride 4 | Playdate Simulator.json 5 | 220818_main_backup.lua 6 | 220818_main_backup.lua 7 | -------------------------------------------------------------------------------- /Source/Images/wall_tiles-table-16-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Source/Images/wall_tiles-table-16-16.png -------------------------------------------------------------------------------- /Wolf3D.pdx/Images/background_gradient.pdi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/Images/background_gradient.pdi -------------------------------------------------------------------------------- /Wolf3D.pdx/Images/minimap_overlay_v2.pdi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMediocritist/Wolf_3D/HEAD/Wolf3D.pdx/Images/minimap_overlay_v2.pdi -------------------------------------------------------------------------------- /Source/pdxinfo: -------------------------------------------------------------------------------- 1 | name=Wolf3D 2 | author=TheMediocritist 3 | description=Demo 3D technique. 4 | bundleID=com.TheMediocritist.Wolf3D 5 | version=1.0 6 | buildNumber=100 7 | imagePath=SystemAssets 8 | -------------------------------------------------------------------------------- /Wolf3D.pdx/pdxinfo: -------------------------------------------------------------------------------- 1 | name=Wolf3D 2 | author=TheMediocritist 3 | description=Demo 3D technique. 4 | bundleID=com.TheMediocritist.Wolf3D 5 | version=1.0 6 | buildNumber=100 7 | imagePath=SystemAssets 8 | pdxversion=11200 9 | buildtime=714627711 10 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | .PHONY: build 3 | .PHONY: run 4 | .PHONY: copy 5 | 6 | SDK = $(shell egrep '^\s*SDKRoot' ~/.Playdate/config | head -n 1 | cut -c9-) 7 | SDKBIN=$(SDK)/bin 8 | GAME=$(notdir $(CURDIR)) 9 | SIM=Playdate Simulator 10 | 11 | 12 | build: clean compile run 13 | 14 | run: open 15 | 16 | clean: 17 | rm -rf '$(GAME).pdx' 18 | 19 | compile: Source/main.lua 20 | "$(SDKBIN)/pdc" 'Source' '$(GAME).pdx' 21 | 22 | open: 23 | open -a '$(SDKBIN)/$(SIM).app/Contents/MacOS/$(SIM)' '$(GAME).pdx' 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wolf 3D 2 | Demonstration of pseudo-3D first person engine for Playdate using Lua. 3 | 4 | The goal here was to build an engine to demonstrate that something _like_ Wolfenstein 3D could reach 30fps by replacing ray-tracing with vertex-projection (I don't know how to describe this; I call it middle-out... ;-P) 5 | 6 | **_Improvements/suggestions are very welcome!_** (I'm just learning how to do this). 7 | 8 | ![Wolf_better_map](https://user-images.githubusercontent.com/79881777/186337397-9e5078c3-201e-4852-ad61-2ab5af2915b4.gif) 9 | 10 | 11 | 12 | Scrappy code but the gist of it is: 13 | * Load a simple map, where 1 = Wall block 14 | * Create sprites for wall tiles and player in mini-map - **_the pseudo-3D all hangs off this_** 15 | * Cast the fewest required number of rays from player to identify viewable wall tiles 16 | * Compare these wall sprite locations to player location to decide which 1 or 2 walls need to be drawn for each sprite 17 | * Project the 2 or 3 vertices that describe these walls into the 3D view and make lines between them 18 | * If vertex is behind player/camera then weird shit happens, intersect the wall with the left or right-most rays from the player and shift the vertex back to the edge of the screen view 19 | * Make wall polygons by mirroring the projected points/lines vertically 20 | * Sort wall polygons from nearest to furthest 21 | * Draw wall polygons 22 | 23 | To do: 24 | - [X] ~~Wall sorting~~ done but now removed as unnecessary 25 | - [X] ~~Occlusion culling~~ done but now removed as unnecessary 26 | - [X] Distance shading 27 | - [X] Implement collisions 28 | - [X] Fix graphical glitch when vertex is _exactly_ at 45 degrees from player (see GIF for example) 29 | - [ ] ~~Build sin & cos lookup tables in init (with lerp function? Or is near enough good enough?)~~ 30 | - [X] Test whether predefined pattern draw faster than ditherPattern (Nope) 31 | - [X] Make the new lineSegment bits for raytrace in init and rotate them instead of generating new in raytrace 32 | - [ ] Make a branch that replaces points and distances with vector2Ds so we can use vector maths and transformations, e.g. by creating view_left and view_right _once_ then rotating it to update 33 | - [ ] Clean up the code 34 | - [X] Implement map scrolling to allow bigger levels (infinite?) 35 | - [X] Replace fixed values with variables for, e.g. FOV, view distance, tile size 36 | - [ ] Replace the 'ray casting' to identify viewable walls with simple stored tile-offset test (8 directions would be plenty) 37 | - [ ] Add option to use 200x120 for drawing then 2x scaling for final 38 | - [X] Think of a better way to deal with occlusion culling because it's only ~1 or 2fps better than just drawing everything 39 | - [ ] Implement doors (think 1 smaller wall tile + 1 sprite door + 1 smaller wall tile) 40 | - [ ] Add demo enemies 41 | - [ ] Running & jumping 42 | - [X] Shooting 43 | - [ ] Get better hobbies 44 | -------------------------------------------------------------------------------- /220818_main_backup.lua: -------------------------------------------------------------------------------- 1 | import 'CoreLibs/sprites' 2 | import 'CoreLibs/graphics' 3 | local gfx = playdate.graphics 4 | local geom = playdate.geometry 5 | local sin = math.sin 6 | local cos = math.cos 7 | local atan2 = math.atan2 8 | local tan = math.tan 9 | local deg = math.deg 10 | local rad = math.rad 11 | 12 | -- set up camera 13 | local camera = {fov = 90, fov_div = 45, view_distance = 70, width = 400, width_div = 200, height = 500, height_div = 250} 14 | 15 | -- performance monitoring (to work out what's using CPU time) 16 | local perf_monitor = table.create(0, 11) 17 | 18 | -- add custom menu items 19 | local menu = playdate.getSystemMenu() 20 | local draw_shaded, draw_debug, perfmon = true, false, false 21 | menu:addCheckmarkMenuItem("Shading", true, function(value) 22 | draw_shaded = value 23 | end) 24 | menu:addCheckmarkMenuItem("draw debug", true, function(value) 25 | draw_debug = value 26 | end) 27 | menu:addCheckmarkMenuItem("perfmon", false, function(value) 28 | perfmon = value 29 | end) 30 | 31 | playdate.setMinimumGCTime(2) -- This is necessary to remove frequent stutters 32 | gfx.setColor(gfx.kColorBlack) 33 | playdate.display.setRefreshRate(40) 34 | 35 | local map_floor1 = 36 | {{0,0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1}, 37 | {0,0,1,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1}, 38 | {0,0,1,0,0,0,0,6,0,0,0,0,0,6,0,0,0,0,0,0,1}, 39 | {0,0,1,1,1,1,1,1,1,1,0,1,1,1,1,0,0,0,0,0,1}, 40 | {0,0,1,0,0,0,0,0,1,1,0,1,1,0,1,1,0,0,0,1,1}, 41 | {0,0,1,0,0,0,0,0,6,0,0,0,1,0,0,1,1,6,1,1,0}, 42 | {0,0,1,0,0,0,0,0,1,1,1,1,1,0,0,0,1,0,1,0,0}, 43 | {0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,0}, 44 | {1,1,1,1,1,6,1,1,1,0,0,0,0,0,0,0,1,0,1,0,0}, 45 | {1,0,0,0,1,0,1,0,0,0,0,0,1,1,1,1,1,0,1,1,1}, 46 | {1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1}, 47 | {1,0,1,1,1,6,1,1,1,0,0,0,1,0,1,1,1,0,1,0,1}, 48 | {1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,0,1,0,1}, 49 | {1,0,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,1,1,1}, 50 | {1,0,6,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,1,0,0}, 51 | {1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,0}, 52 | {1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0}, 53 | {1,0,1,1,1,6,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0}, 54 | {1,0,1,0,1,0,1,0,1,1,0,0,0,0,0,1,1,1,1,1,1}, 55 | {1,0,0,0,1,0,1,0,0,1,1,1,1,1,1,1,1,1,0,0,1}, 56 | {1,0,1,0,1,0,1,0,0,1,1,1,1,0,0,0,0,1,0,4,1}, 57 | {1,0,1,1,1,0,1,1,1,1,0,0,0,0,0,0,0,6,0,0,1}, 58 | {1,0,0,0,0,0,0,0,0,6,0,0,1,0,0,0,0,1,0,0,1}, 59 | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}} 60 | 61 | local map = { 62 | 1, 1, 1, 1, 1, 1, 1, 63 | 1, 0, 0, 0, 0, 0, 1, 64 | 1, 0, 1, 1, 0, 1, 1, 65 | 1, 0, 0, 0, 0, 0, 1, 66 | 1, 0, 1, 0, 1, 0, 1, 67 | 1, 0, 1, 0, 1, 0, 1, 68 | 1, 1, 1, 1, 1, 1, 1 69 | } 70 | 71 | local working_map_rows, working_map_columns = nil, nil 72 | local working_map_sprites = {} 73 | 74 | local initialised = false 75 | local map_sprite, player_sprite = nil, nil 76 | local sprite_size = 16 77 | local wall_sprites = {} 78 | local player_start = {x = 24, y = 24, direction = 90} 79 | --local rays = {} 80 | local draw_these = {} 81 | local view = gfx.image.new(400, 240, gfx.kColorBlack) 82 | local background_image = gfx.image.new('Images/background_gradient') 83 | --local images = {} 84 | local wall_tiles_imagetable = gfx.imagetable.new("Images/wall_tiles-table-16-16") 85 | 86 | function isWall(tile_x, tile_y) 87 | -- returns true if working map has a wall at tile_x, tile_y 88 | if working_map[(tile_y - 1) * 7 + tile_x] == 1 then 89 | return true 90 | else 91 | return false 92 | end 93 | end 94 | 95 | function tileAt(x, y) 96 | -- returns: tileid, column, row 97 | -- or false if outside working map bounds 98 | local column, row = math.ceil(x/16), math.ceil(y/16) 99 | if column > 0 and column <= working_map_columns and row > 0 and row <= working_map_rows then 100 | local tileid = (row - 1) * working_map_columns + column 101 | return tileid, column, row 102 | else 103 | return false 104 | end 105 | end 106 | 107 | function spritesAt(column, row) 108 | -- returns {true, {sprite list} if mini map has a sprite at tile_x, tile_y 109 | local x = (column - 1) * 16 + 8 110 | local y = (row - 1) * 16 + 8 111 | local sprites_at_point = gfx.sprite.querySpritesAtPoint(x, y) 112 | if #sprites_at_point > 0 then 113 | return true, sprites_at_point 114 | end 115 | end 116 | 117 | function makeWorkingMap(columns, rows) 118 | working_map_sprites = {} 119 | local index = 0 120 | for y = 1, rows do 121 | for x = 1, columns do 122 | index += 1 123 | -- put a tile sprite here 124 | local sprite = gfx.sprite.new() 125 | sprite.tileid = index 126 | sprite.row = y 127 | sprite.column = x 128 | sprite.width, sprite.height = sprite_size, sprite_size 129 | sprite.is_wall = false 130 | working_map_sprites[#working_map_sprites + 1] = sprite 131 | end 132 | end 133 | end 134 | 135 | function initialise() 136 | --makeWorkingMap(12, 12) 137 | setupPerformanceMonitor() 138 | makeWallImages() 139 | makeWallSprites(map, 7, 7) 140 | player_sprite = makePlayer(player_start.x, player_start.y, player_start.direction) 141 | setUpCamera() 142 | initialised = true 143 | 144 | gfx.sprite.setBackgroundDrawingCallback( 145 | function() 146 | view:draw(0, 0) 147 | end 148 | ) 149 | end 150 | 151 | function makeWallImages () 152 | 153 | images.walls_noview = {} 154 | images.walls_inview = {} 155 | images.walls_noview.three_n = wall_tiles_imagetable:getImage(12) 156 | images.walls_noview.two_ne = wall_tiles_imagetable:getImage(8) 157 | images.walls_noview.two_ns = wall_tiles_imagetable:getImage(12) 158 | images.walls_noview.one_nes = wall_tiles_imagetable:getImage(2) 159 | images.walls_noview.four_nesw = wall_tiles_imagetable:getImage(1) 160 | 161 | images.walls_inview.three_n = wall_tiles_imagetable:getImage(27) 162 | images.walls_inview.two_ne = wall_tiles_imagetable:getImage(23) 163 | images.walls_inview.two_ns = wall_tiles_imagetable:getImage(27) 164 | images.walls_inview.one_nes = wall_tiles_imagetable:getImage(17) 165 | images.walls_inview.four_nesw = wall_tiles_imagetable:getImage(16) 166 | 167 | end 168 | 169 | function setUpCamera() 170 | print("fov: " .. camera.fov) 171 | 172 | -- calculate smallest number of rays required to detect all tiles in range of camera view_distance 173 | local required_angle = math.deg(math.atan(sprite_size/camera.view_distance)) 174 | local camera_rays = math.floor(camera.fov/required_angle) -- Temp until rays replaced with tree 175 | camera.ray_angles = camera.fov/camera_rays 176 | camera.rays = camera_rays + 1 -- fence segments vs posts 177 | camera.direction = player_sprite.direction 178 | camera.ray_lines = table.create(camera.rays, 0) 179 | print("FOV: " .. camera.fov .. ", " .. camera.rays .. " rays at intervals of " .. math.floor(camera.ray_angles * 100)/100 .. " degrees") 180 | for i = 0, camera.rays do 181 | local ray_direction = (player_sprite.direction - camera.fov_div) + (camera.ray_angles * i) 182 | local ray_end_x = player_sprite.x + camera.view_distance * sin_rad(ray_direction)) 183 | local ray_end_y = player_sprite.y - camera.view_distance * cos_rad(ray_direction)) 184 | camera.ray_lines[i+1] = geom.lineSegment.new(player_sprite.x, player_sprite.y, ray_end_x, ray_end_y) 185 | end 186 | printTable(camera.ray_lines) 187 | 188 | 189 | end 190 | 191 | 192 | function playdate.update() 193 | if initialised == false then initialise() end 194 | 195 | updateView() 196 | 197 | gfx.sprite.redrawBackground() 198 | 199 | if perfmon then 200 | playdate.resetElapsedTime() 201 | end 202 | gfx.sprite.update() 203 | if perfmon then 204 | perf_monitor.sprites_update.finish = playdate.getElapsedTime() 205 | playdate.resetElapsedTime() 206 | end 207 | 208 | for i = 1, camera.rays, (camera.rays -1) do 209 | gfx.setLineWidth(3) 210 | gfx.setColor(gfx.kColorWhite) 211 | gfx.drawLine(camera.ray_lines[i]) 212 | gfx.setLineWidth(1) 213 | gfx.setColor(gfx.kColorBlack) 214 | gfx.drawLine(camera.ray_lines[i]) 215 | end 216 | 217 | if perfmon then 218 | perf_monitor.sprites_draw.finish = playdate.getElapsedTime() 219 | end 220 | playdate.drawFPS(0,0) 221 | end 222 | 223 | function updateView() 224 | 225 | gfx.lockFocus(view) 226 | background_image:draw(0, 0) 227 | 228 | if perfmon then 229 | gfx.setColor(gfx.kColorWhite) 230 | gfx.fillRect(215, 5, 180, 230) 231 | gfx.setColor(gfx.kColorBlack) 232 | gfx.drawText("player: " .. perf_monitor.player_update.finish*1000 .. "ms", 220, 10) 233 | gfx.drawText("load vert: " .. perf_monitor.projection_load_vertices.finish*1000 .. "ms", 220, 30) 234 | gfx.drawText("vert math: " .. perf_monitor.projection_vertex_maths.finish*1000 .. "ms", 220, 50) 235 | gfx.drawText("vert clip: " .. perf_monitor.projection_vertex_clip.finish*1000 .. "ms", 220, 70) 236 | gfx.drawText("vert proj: " .. perf_monitor.projection_vertex_project.finish*1000 .. "ms", 220, 90) 237 | gfx.drawText("poly make: " .. perf_monitor.projection_poly_make.finish*1000 .. "ms", 220, 110) 238 | gfx.drawText("poly sort: " .. perf_monitor.projection_poly_sort.finish*1000 .. "ms", 220, 130) 239 | gfx.drawText("poly cull: " .. perf_monitor.projection_poly_cull.finish*1000 .. "ms", 220, 150) 240 | gfx.drawText("poly draw: " .. perf_monitor.projection_poly_draw.finish*1000 .. "ms", 220, 170) 241 | gfx.drawText("sprite upd: " .. perf_monitor.projection_poly_draw.finish*1000 .. "ms", 220, 190) 242 | gfx.drawText("draw: " .. perf_monitor.projection_poly_draw.finish*1000 .. "ms", 220, 210) 243 | end 244 | 245 | local screen_polys = table.create(#draw_these, 0) 246 | local player = geom.point.new(player_sprite.x, player_sprite.y) 247 | 248 | local num_draw_these = #draw_these 249 | 250 | if perfmon then 251 | perf_monitor.projection_load_vertices.start = playdate.getCurrentTimeMilliseconds() 252 | playdate.resetElapsedTime() 253 | end 254 | 255 | for i = 1, num_draw_these do 256 | --local wall_sprite = draw_these[i] 257 | local points = getVertices(draw_these[i]) 258 | 259 | if perfmon then 260 | perf_monitor.projection_load_vertices.finish = playdate.getElapsedTime() * num_draw_these 261 | perf_monitor.projection_vertex_maths.start = playdate.getCurrentTimeMilliseconds() 262 | playdate.resetElapsedTime() 263 | end 264 | 265 | local p = table.create(#points, 0) 266 | for i = 1, #points do 267 | p[i] = { vertex = points[i] } 268 | end 269 | 270 | local p1_obj = p[1] 271 | local p2_obj = p[2] 272 | 273 | local last_p = #p 274 | if last_p > 0 then 275 | for i = 1, last_p do 276 | p[i].delta = player - p[i].vertex 277 | local deltax, deltay = p[i].delta:unpack() 278 | p[i].player_angle = deg(atan2(deltax, -deltay)) +180 279 | --if p[i].player_angle < 0 then p[i].player_angle += 360 end 280 | p[i].camera_angle = (p[i].player_angle - player_sprite.direction) % 360 281 | if p[i].camera_angle > 180 then p[i].camera_angle -= 360 end 282 | end 283 | 284 | if last_p == 3 then 285 | if p1_obj.camera_angle <= -(camera.fov_div) and p2_obj.camera_angle <= -(camera.fov_div) then 286 | table.remove(p, 1) 287 | last_p -= 1 288 | end 289 | 290 | if p[last_p].camera_angle >= (camera.fov_div) and p[last_p-1].camera_angle >= (camera.fov_div) then 291 | table.remove(p, last_p) 292 | last_p -= 1 293 | end 294 | end 295 | end 296 | 297 | 298 | if last_p > 0 then 299 | 300 | for i = 1, last_p do 301 | local p = p[i] 302 | p.player_distance = p.vertex:distanceToPoint(player) 303 | p.camera_distance = p.player_distance * cos(rad(p.camera_angle)) 304 | end 305 | 306 | if perfmon then 307 | perf_monitor.projection_vertex_maths.finish = playdate.getElapsedTime() * num_draw_these 308 | perf_monitor.projection_vertex_clip.start = playdate.getCurrentTimeMilliseconds() 309 | playdate.resetElapsedTime() 310 | end 311 | 312 | if p1_obj.camera_angle < -(camera.fov_div) and p[1].camera_distance < sprite_size then 313 | local x3, y3, x4, y4 = camera.ray_lines[1]:unpack() 314 | local intersects, new_point_x, new_point_y = geom.lineSegment.fast_intersection(p2_obj.vertex.x, p2_obj.vertex.y, p1_obj.vertex.x, p1_obj.vertex.y, x3, y3, x4, y4) 315 | 316 | if intersects then 317 | p1_obj.vertex = geom.point.new(new_point_x, new_point_y) 318 | p1_obj.delta = p1_obj.vertex - player 319 | p1_obj.player_distance = p1_obj.vertex:distanceToPoint(player) 320 | p1_obj.camera_angle = -(camera.fov_div) 321 | p1_obj.camera_distance = p1_obj.player_distance * cos(rad(p1_obj.camera_angle)) 322 | end 323 | 324 | elseif p1_obj.camera_angle > ((camera.fov_div)) and p[1].camera_distance < sprite_size then 325 | local x3, y3, x4, y4 = camera.ray_lines[#camera.ray_lines]:unpack() 326 | local intersects, new_point_x, new_point_y = geom.lineSegment.fast_intersection(p2_obj.vertex.x, p2_obj.vertex.y, p1_obj.vertex.x, p1_obj.vertex.y, x3, y3, x4, y4) 327 | 328 | if intersects then 329 | p1_obj.vertex = geom.point.new(new_point_x, new_point_y) 330 | p1_obj.delta = p1_obj.vertex - player 331 | p1_obj.player_distance = p1_obj.vertex:distanceToPoint(player) 332 | p1_obj.camera_angle = (camera.fov_div) 333 | p1_obj.camera_distance = p1_obj.player_distance * cos(rad(p1_obj.camera_angle)) 334 | end 335 | end 336 | 337 | local last_point_obj = p[#p] 338 | local last_last_point_obj = p[#p-1] 339 | if last_point_obj.camera_angle < (-(camera.fov_div)) and last_point_obj.camera_distance < sprite_size then 340 | local x3, y3, x4, y4 = camera.ray_lines[1]:unpack() 341 | local intersects, new_point_x, new_point_y = geom.lineSegment.fast_intersection(last_point_obj.vertex.x, last_point_obj.vertex.y, last_last_point_obj.vertex.x, last_last_point_obj.vertex.y, x3, y3, x4, y4) 342 | 343 | if intersects then 344 | last_point_obj.vertex = geom.point.new(new_point_x, new_point_y) 345 | last_point_obj.delta = last_point_obj.vertex - player 346 | last_point_obj.player_distance = last_point_obj.vertex:distanceToPoint(player) 347 | last_point_obj.camera_angle = -(camera.fov_div) 348 | last_point_obj.camera_distance = last_point_obj.player_distance * cos(rad(last_point_obj.camera_angle)) 349 | end 350 | elseif last_point_obj.camera_angle > camera.fov_div and last_point_obj.camera_distance < sprite_size then 351 | local x3, y3, x4, y4 = camera.ray_lines[#camera.ray_lines]:unpack() 352 | local intersects, new_point_x, new_point_y = geom.lineSegment.fast_intersection(last_point_obj.vertex.x, last_point_obj.vertex.y, last_last_point_obj.vertex.x, last_last_point_obj.vertex.y, x3, y3, x4, y4) 353 | if intersects then 354 | last_point_obj.vertex = geom.point.new(new_point_x, new_point_y) 355 | last_point_obj.delta = last_point_obj.vertex - player 356 | last_point_obj.player_distance = last_point_obj.vertex:distanceToPoint(player) 357 | last_point_obj.camera_angle = (camera.fov_div) 358 | last_point_obj.camera_distance = last_point_obj.player_distance * cos(rad(last_point_obj.camera_angle)) 359 | end 360 | end 361 | 362 | if perfmon then 363 | perf_monitor.projection_vertex_clip.finish = playdate.getElapsedTime() * num_draw_these 364 | playdate.resetElapsedTime() 365 | end 366 | 367 | for i = 1, last_p do 368 | p[i].offset_x = (p[i].camera_angle/(camera.fov_div)) * (camera.width_div) 369 | p[i].offset_y = (1/p[i].camera_distance) * (camera.height_div) 370 | end 371 | 372 | if perfmon then 373 | perf_monitor.projection_vertex_project.finish = playdate.getElapsedTime() * num_draw_these 374 | playdate.resetElapsedTime() 375 | end 376 | 377 | local last_point = #p 378 | 379 | for i = 1, last_point - 1 do 380 | screen_polys[#screen_polys+1] = table.create(0, 4) 381 | local p_obj = p[i] 382 | local p_plus = p[i+1] 383 | local poly = screen_polys[#screen_polys] 384 | poly.distance = (p_obj.camera_distance + p_plus.camera_distance)/2 385 | poly.left_angle = math.min(p_obj.camera_angle, p_plus.camera_angle) 386 | poly.right_angle = math.max(p_obj.camera_angle, p_plus.camera_angle) 387 | 388 | poly.polygon = geom.polygon.new( 389 | 200 + p_obj.offset_x, 120 + p_obj.offset_y*4, 390 | 200 + p_plus.offset_x, 120 + p_plus.offset_y*4, 391 | 200 + p_plus.offset_x, 120 - p_plus.offset_y*4, 392 | 200 + p_obj.offset_x, 120 - p_obj.offset_y*4, 393 | 200 + p_obj.offset_x, 120 + p_obj.offset_y*4) 394 | 395 | if draw_debug then 396 | -- draw wall to top-down view 397 | gfx.setColor(gfx.kColorWhite) 398 | gfx.drawLine( 200 + p_obj.camera_distance * tan(rad(p_obj.camera_angle)), 128 - p_obj.camera_distance, 399 | 200 + p_plus.camera_distance * tan(rad(p_plus.camera_angle)), 128 - p_plus.camera_distance) 400 | gfx.setColor(gfx.kColorBlack) 401 | end 402 | end 403 | if perfmon then 404 | perf_monitor.projection_poly_make.finish = playdate.getElapsedTime() * num_draw_these 405 | playdate.resetElapsedTime() 406 | end 407 | end 408 | end 409 | 410 | -- Draw polygons 411 | 412 | local num_screen_polys = #screen_polys 413 | 414 | -- if sort_polys == true and num_screen_polys > 0 then 415 | -- table.sort(screen_polys, function (k1, k2) return k1.distance < k2.distance end) 416 | -- end 417 | -- 418 | -- if perfmon then 419 | -- perf_monitor.projection_poly_sort.finish = playdate.getElapsedTime() 420 | -- playdate.resetElapsedTime() 421 | -- end 422 | -- 423 | -- if cull_polys == true then 424 | -- if num_screen_polys > 0 then 425 | -- -- determine if near polygons are blocking view of far polygons and if so, remove 426 | -- local blocked_area = table.create(num_screen_polys, 0) 427 | -- blocked_area[#blocked_area + 1] = table.create(0, 2) 428 | -- blocked_area[1].left = screen_polys[1].left_angle 429 | -- blocked_area[1].right = screen_polys[1].right_angle 430 | -- 431 | -- for i = 2, num_screen_polys do 432 | -- local done = false 433 | -- for j = 1, #blocked_area do 434 | -- if screen_polys[i].left_angle >= blocked_area[j].left and screen_polys[i].right_angle <= blocked_area[j].right then 435 | -- screen_polys[i].delete = true 436 | -- done = true 437 | -- elseif screen_polys[i].left_angle <= blocked_area[j].left and screen_polys[i].right_angle >= blocked_area[j].left then 438 | -- blocked_area[j].left = screen_polys[i].left_angle 439 | -- done = true 440 | -- elseif screen_polys[i].right_angle >= blocked_area[j].right and screen_polys[i].left_angle <= blocked_area[j].right then 441 | -- blocked_area[j].right = screen_polys[i].right_angle 442 | -- done = true 443 | -- end 444 | -- end 445 | -- 446 | -- if done == false then 447 | -- blocked_area[#blocked_area + 1] = table.create(0, 2) 448 | -- blocked_area[#blocked_area].left = screen_polys[i].left_angle 449 | -- blocked_area[#blocked_area].right = screen_polys[i].right_angle 450 | -- end 451 | -- end 452 | -- 453 | -- for i = num_screen_polys, 1, -1 do 454 | -- if screen_polys[i].delete == true then 455 | -- table.remove(screen_polys, i) 456 | -- num_screen_polys -= 1 457 | -- end 458 | -- end 459 | -- end 460 | -- end 461 | if perfmon then 462 | perf_monitor.projection_poly_cull.finish = playdate.getElapsedTime() 463 | playdate.resetElapsedTime() 464 | end 465 | 466 | if draw_shaded == false then 467 | gfx.setColor(gfx.kColorWhite) 468 | for i = num_screen_polys, 1, -1 do 469 | gfx.drawPolygon(screen_polys[i].polygon) 470 | end 471 | else 472 | 473 | for i = num_screen_polys, 1, -1 do 474 | gfx.setColor(gfx.kColorWhite) 475 | gfx.setDitherPattern(0.1+(screen_polys[i].distance/80),gfx.image.kDitherTypeBayer4x4) 476 | gfx.fillPolygon(screen_polys[i].polygon) 477 | end 478 | gfx.setColor(gfx.kColorBlack) 479 | end 480 | 481 | if perfmon then 482 | perf_monitor.projection_poly_draw.finish = playdate.getElapsedTime() 483 | end 484 | 485 | if draw_debug then 486 | gfx.setColor(gfx.kColorWhite) 487 | gfx.drawLine(200, 128, 152, 80) 488 | gfx.drawLine(200, 128, 248, 80) 489 | end 490 | 491 | gfx.setColor(gfx.kColorBlack) 492 | gfx.unlockFocus() 493 | 494 | end 495 | 496 | function makeWallSprites(map, columns, rows) 497 | local map_index = 0 498 | local image_outofview = gfx.image.new(16, 16, gfx.kColorBlack) 499 | local image_inview = gfx.image.new(16, 16, gfx.kColorBlack) 500 | gfx.lockFocus(image_inview) 501 | gfx.setColor(gfx.kColorWhite) 502 | gfx.drawRect(1, 1, 14, 14) 503 | gfx.unlockFocus() 504 | gfx.setColor(gfx.kColorBlack) 505 | 506 | for y = 1, rows do 507 | for x = 1, columns do 508 | map_index += 1 509 | if map[map_index] == 1 then 510 | local s = gfx.sprite.new(16,16) 511 | s.image_inview = image_inview 512 | s.image_noview = image_outofview 513 | s.inview = false 514 | s.wall = true 515 | s.index = map_index 516 | local vertices = {nw = geom.point.new((x-1) * 16, (y-1) * 16), 517 | ne = geom.point.new(x * 16, (y-1) * 16), 518 | se = geom.point.new(x * 16, y * 16), 519 | sw = geom.point.new((x-1) * 16, y * 16)} 520 | s.view_vertices = {} 521 | 522 | local num_walls = 4 523 | 524 | -- cull walls between wall sprites and populate view vertices (8 directions) 525 | if y == 1 or (y > 1 and map[(y - 2) * columns + x] == 1) then s.wall_n = true num_walls -=1 else s.wall_n = false end 526 | if y == 7 or (y < 7 and map[y * columns + x] == 1) then s.wall_s = true num_walls -=1 else s.wall_s = false end 527 | if x == 1 or (x > 1 and map[(y - 1) * columns + x - 1] == 1) then s.wall_w = true num_walls -=1 else s.wall_w = false end 528 | if x == 7 or (x < 7 and map[(y - 1) * columns + x + 1] == 1) then s.wall_e = true num_walls -=1 else s.wall_e = false end 529 | 530 | if num_walls == 4 then 531 | s.image_noview = wall_tiles_imagetable:getImage(1) 532 | s.image_inview = wall_tiles_imagetable:getImage(16) 533 | elseif num_walls == 3 then 534 | if s.wall_n then 535 | s.image_noview = wall_tiles_imagetable:getImage(2) 536 | s.image_inview = wall_tiles_imagetable:getImage(17) 537 | elseif s.wall_e then 538 | s.image_noview = wall_tiles_imagetable:getImage(3) 539 | s.image_inview = wall_tiles_imagetable:getImage(18) 540 | elseif s.wall_s then 541 | s.image_noview = wall_tiles_imagetable:getImage(4) 542 | s.image_inview = wall_tiles_imagetable:getImage(19) 543 | elseif s.wall_w then 544 | s.image_noview = wall_tiles_imagetable:getImage(5) 545 | s.image_inview = wall_tiles_imagetable:getImage(20) 546 | end 547 | elseif num_walls == 2 then 548 | if s.wall_s and s.wall_w then 549 | s.image_noview = wall_tiles_imagetable:getImage(6) 550 | s.image_inview = wall_tiles_imagetable:getImage(21) 551 | elseif s.wall_w and s.wall_n then 552 | s.image_noview = wall_tiles_imagetable:getImage(7) 553 | s.image_inview = wall_tiles_imagetable:getImage(22) 554 | elseif s.wall_n and s.wall_e then 555 | s.image_noview = wall_tiles_imagetable:getImage(8) 556 | s.image_inview = wall_tiles_imagetable:getImage(23) 557 | elseif s.wall_e and s.wall_s then 558 | s.image_noview = wall_tiles_imagetable:getImage(9) 559 | s.image_inview = wall_tiles_imagetable:getImage(24) 560 | elseif s.wall_n and s.wall_s then 561 | s.image_noview = wall_tiles_imagetable:getImage(10) 562 | s.image_inview = wall_tiles_imagetable:getImage(25) 563 | elseif s.wall_e and s.wall_w then 564 | s.image_noview = wall_tiles_imagetable:getImage(11) 565 | s.image_inview = wall_tiles_imagetable:getImage(26) 566 | end 567 | elseif num_walls == 1 then 568 | if s.wall_e and s.wall_s and s.wall_w then 569 | s.image_noview = wall_tiles_imagetable:getImage(12) 570 | s.image_inview = wall_tiles_imagetable:getImage(27) 571 | elseif s.wall_s and s.wall_w and s.wall_n then 572 | s.image_noview = wall_tiles_imagetable:getImage(13) 573 | s.image_inview = wall_tiles_imagetable:getImage(28) 574 | elseif s.wall_w and s.wall_n and s.wall_e then 575 | s.image_noview = wall_tiles_imagetable:getImage(14) 576 | s.image_inview = wall_tiles_imagetable:getImage(29) 577 | elseif s.wall_n and s.wall_e and s.wall_s then 578 | s.image_noview = wall_tiles_imagetable:getImage(15) 579 | s.image_inview = wall_tiles_imagetable:getImage(30) 580 | end 581 | end 582 | 583 | if not (s.wall_n and s.wall_s and s.wall_e and s.wall_w) then 584 | 585 | -- when wall is below and right of player, draw left and top sides 586 | if s.wall_n and s.wall_w then s.view_vertices.nw = table.create(2, 0) 587 | elseif s.wall_n then s.view_vertices.nw = {vertices.nw, vertices.sw} 588 | elseif s.wall_w then s.view_vertices.nw = {vertices.ne, vertices.nw} 589 | else s.view_vertices.nw = {vertices.ne, vertices.nw, vertices.sw} 590 | end 591 | 592 | -- when wall is above and right of player, draw left and bottom sides 593 | if s.wall_w and s.wall_s then s.view_vertices.sw = table.create(2, 0) 594 | elseif s.wall_w then s.view_vertices.sw = {vertices.sw, vertices.se} 595 | elseif s.wall_s then s.view_vertices.sw = {vertices.nw, vertices.sw} 596 | else s.view_vertices.sw = {vertices.nw, vertices.sw, vertices.se} 597 | end 598 | 599 | -- when wall is below and left of player, draw right and top sides 600 | if s.wall_n and s.wall_e then s.view_vertices.ne = table.create(2, 0) 601 | elseif s.wall_n then s.view_vertices.ne = {vertices.se, vertices.ne} 602 | elseif s.wall_e then s.view_vertices.ne = {vertices.ne, vertices.nw} 603 | else s.view_vertices.ne = {vertices.se, vertices.ne, vertices.nw} 604 | end 605 | 606 | -- when wall is above and left of player, draw right and bottom sides 607 | if s.wall_e and s.wall_s then s.view_vertices.se = table.create(2, 0) 608 | elseif s.wall_e then s.view_vertices.se = {vertices.sw, vertices.se} 609 | elseif s.wall_s then s.view_vertices.se = {vertices.se, vertices.ne} 610 | else s.view_vertices.se = {vertices.sw, vertices.se, vertices.ne} 611 | end 612 | 613 | -- when wall is directly below player, only draw the top side 614 | if s.wall_n then s.view_vertices.n = table.create(2, 0) 615 | else s.view_vertices.n = {vertices.ne, vertices.nw} 616 | end 617 | 618 | -- when wall is directly above player, only draw the bottom side 619 | if s.wall_s then s.view_vertices.s = table.create(2, 0) 620 | else s.view_vertices.s = {vertices.sw, vertices.se} 621 | end 622 | 623 | -- when wall is directly to right of player, only draw the left side 624 | if s.wall_w then s.view_vertices.w = table.create(2, 0) 625 | else s.view_vertices.w = {vertices.nw, vertices.sw} 626 | end 627 | 628 | -- when wall is directly to left of player, only draw the right side 629 | if s.wall_e then s.view_vertices.e = table.create(2, 0) 630 | else s.view_vertices.e = {vertices.se, vertices.ne} 631 | end 632 | 633 | s:setCollideRect(0, 0, 16, 16) 634 | 635 | function s.update() 636 | if s.inview == true and s:getImage() ~= s.image_inview then 637 | s:setImage(s.image_inview) 638 | elseif s.inview == false and s:getImage() ~= s.image_noview then 639 | s:setImage(s.image_noview) 640 | s.inview = false 641 | else 642 | s.inview = false 643 | end 644 | end 645 | 646 | s:add() 647 | s:moveTo((x-1) * 16+8, (y-1) * 16+8) 648 | 649 | wall_sprites[#wall_sprites + 1] = s 650 | end 651 | end 652 | end 653 | 654 | end 655 | end 656 | 657 | function makePlayer(x_pos, y_pos, direction) 658 | 659 | local image = gfx.image.new(6, 6) 660 | gfx.lockFocus(image) 661 | gfx.fillCircleAtPoint(3, 3, 3) 662 | gfx.setColor(gfx.kColorWhite) 663 | gfx.fillCircleAtPoint(3, 30, 2) 664 | 665 | gfx.unlockFocus() 666 | local s = gfx.sprite.new(image) 667 | s.moved = false 668 | s.direction = direction 669 | s:setCollideRect(0, 0, 6, 6) 670 | s:setCenter(0.5, 0.5) 671 | s.collisionResponse = gfx.sprite.kCollisionTypeSlide 672 | s.rotate_transform = playdate.geometry.affineTransform.new() 673 | s.sin_dir = sin(rad(s.direction)) 674 | s.cos_dir = cos(rad(s.direction)) 675 | function s:update() 676 | if perfmon then 677 | playdate.resetElapsedTime() 678 | end 679 | local movex, movey = 0, 0 680 | if playdate.buttonIsPressed('right') then 681 | if playdate.buttonIsPressed('b') then 682 | -- strafe right 683 | movex = s.cos_dir 684 | movey = -s.sin_dir 685 | s.moved = true 686 | else 687 | -- turn right 688 | s.direction += 4 689 | s.rotate_transform:rotate(4, s.x, s.y) 690 | if s.direction > 360 then s.direction -= 360 end 691 | s.moved = true 692 | end 693 | end 694 | if playdate.buttonIsPressed('left') then 695 | if playdate.buttonIsPressed('b') then 696 | -- strafe left 697 | movex = -s.cos_dir 698 | movey = s.sin_dir 699 | s.moved = true 700 | else 701 | -- turn left 702 | s.direction -= 4 703 | s.rotate_transform:rotate(-4, s.x, s.y) 704 | if s.direction < 0 then s.direction += 360 end 705 | s.moved = true 706 | end 707 | end 708 | if playdate.buttonIsPressed('up') then 709 | movex = s.sin_dir 710 | movey = s.cos_dir 711 | s.moved = true 712 | end 713 | if playdate.buttonIsPressed('down') then 714 | movex = -s.sin_dir 715 | movey = -s.cos_dir 716 | s.moved = true 717 | end 718 | 719 | if s.moved then 720 | local actualX, actualY, collisions = s:moveWithCollisions(s.x + movex, s.y - movey) 721 | for i = 1, #camera.ray_lines do 722 | camera.ray_lines[i]:offset(-(camera.ray_lines[i].x - actualX), -(camera.ray_lines[i].y - actualY)) 723 | camera.ray_lines[i] = s.rotate_transform:transformedLineSegment(camera.ray_lines[i]) 724 | end 725 | 726 | s.sin_dir = sin(rad(s.direction)) 727 | s.cos_dir = cos(rad(s.direction)) 728 | 729 | s.moved = false 730 | s.rotate_transform:reset() 731 | end 732 | 733 | if perfmon then 734 | perf_monitor.player_update.finish = playdate.getElapsedTime() 735 | playdate.resetElapsedTime() 736 | end 737 | 738 | s:raytrace() 739 | 740 | if perfmon then 741 | perf_monitor.player_find_viewable_walls.finish = playdate.getElapsedTime() 742 | end 743 | 744 | end 745 | 746 | function s:raytrace() 747 | draw_these = {} 748 | -- trace rays 749 | for i = 1, camera.rays do 750 | ray_hits = gfx.sprite.querySpritesAlongLine(camera.ray_lines[i]) 751 | for i = 1, math.min(#ray_hits, 3) do 752 | ray_hits[i].inview = true 753 | end 754 | end 755 | for i = 1, #wall_sprites do 756 | if wall_sprites[i].inview then 757 | draw_these[#draw_these + 1] = wall_sprites[i] 758 | end 759 | end 760 | end 761 | 762 | -- function s:tileSelect(angle) 763 | -- draw_these = {} 764 | -- 765 | -- if angle >= 337.5 or angle < 22.5 then 766 | -- local view_tiles = table.create(22, 0) 767 | -- -- heading north 768 | -- view_tiles = {[1] = true, [2] = true, [3] = true, [4] = true, [5] = true, [6] = true, [7] = true, [8] = true, [9] = true, [10] = true, [11] = true, [12] = true, [13] = true, [14] = true, [16] = true, [17] = true, [18] = true, [19] = true, [20] = true, [24] = true, [25] = true, [26] = true} 769 | -- 770 | -- for i = 1, #wall_sprites do 771 | -- if view_tiles[wall_sprites[i].index] then 772 | -- wall_sprites[i].inview = true 773 | -- draw_these[#draw_these + 1] = wall_sprites[i] 774 | -- end 775 | -- end 776 | -- 777 | -- view_tiles = nil 778 | -- end 779 | -- end 780 | 781 | s:add() 782 | s:moveTo(x_pos, y_pos) 783 | return s 784 | 785 | end 786 | 787 | 788 | function setupPerformanceMonitor() 789 | 790 | perf_monitor.player_update = {start = 0, finish = 0, ms = 0, perc = 0} 791 | perf_monitor.player_find_viewable_walls = {start = 0, finish = 0, ms = 0, perc = 0} 792 | 793 | perf_monitor.projection_load_vertices = {start = 0, finish = 0, ms = 0, perc = 0} 794 | perf_monitor.projection_vertex_maths = {start = 0, finish = 0, ms = 0, perc = 0} 795 | perf_monitor.projection_vertex_project = {start = 0, finish = 0, ms = 0, perc = 0} 796 | perf_monitor.projection_vertex_clip = {start = 0, finish = 0, ms = 0, perc = 0} 797 | perf_monitor.projection_poly_make = {start = 0, finish = 0, ms = 0, perc = 0} 798 | perf_monitor.projection_poly_sort = {start = 0, finish = 0, ms = 0, perc = 0} 799 | perf_monitor.projection_poly_cull = {start = 0, finish = 0, ms = 0, perc = 0} 800 | perf_monitor.projection_poly_draw = {start = 0, finish = 0, ms = 0, perc = 0} 801 | 802 | perf_monitor.sprites_update = {start = 0, finish = 0, ms = 0, perc = 0} 803 | perf_monitor.sprites_draw = {start = 0, finish = 0, ms = 0, perc = 0} 804 | 805 | end 806 | 807 | function getVertices(wall_sprite) 808 | -- Fetch vertices for projecting/drawing 809 | if wall_sprite.x - 8 > player_sprite.x then 810 | if wall_sprite.y - 8 > player_sprite.y then return wall_sprite.view_vertices.nw -- wall is below and right of player 811 | elseif (wall_sprite.y + 8) < player_sprite.y then return wall_sprite.view_vertices.sw -- wall is above and right of player 812 | else return wall_sprite.view_vertices.w end -- wall is directly to right of player 813 | elseif (wall_sprite.x + 8) < player_sprite.x then 814 | if wall_sprite.y - 8 > player_sprite.y then return wall_sprite.view_vertices.ne -- wall is below and left of player 815 | elseif (wall_sprite.y + 8) < player_sprite.y then return wall_sprite.view_vertices.se -- wall is above and left of player 816 | else return wall_sprite.view_vertices.e end -- wall is directly to left of player 817 | elseif (wall_sprite.y - 8) > player_sprite.y then return wall_sprite.view_vertices.n -- wall is directly below player 818 | elseif (wall_sprite.y + 8) < player_sprite.y then return wall_sprite.view_vertices.s -- wall is directly above player 819 | end 820 | end -------------------------------------------------------------------------------- /Source/main.lua: -------------------------------------------------------------------------------- 1 | import 'CoreLibs/sprites' 2 | import 'CoreLibs/graphics' 3 | import 'CoreLibs/animation' 4 | import 'CoreLibs/timer' 5 | 6 | local gfx = playdate.graphics 7 | local geom = playdate.geometry 8 | local sin = math.sin 9 | local cos = math.cos 10 | local atan = math.atan 11 | local atan2 = math.atan2 12 | local tan = math.tan 13 | local deg = math.deg 14 | local rad = math.rad 15 | local asin = math.asin 16 | local ceil = math.ceil 17 | local floor = math.floor 18 | local min = math.min 19 | local max = math.max 20 | local pow = math.pow 21 | local fast_intersection = geom.lineSegment.fast_intersection 22 | 23 | -- set up camera 24 | local camera = {fov = 70, view_distance = 80, width = 400, width_div = 200, height = 500, height_div = 250} 25 | local camera_width_half = camera.width / 2 26 | local camera_height_half = camera.height / 2 27 | local camera_fov_half = camera.fov / 2 28 | local camera_fov_half_neg = -camera_fov_half 29 | 30 | -- variables to store dt/delta time 31 | local dt, last_time = 0, 0 32 | 33 | -- add custom menu items 34 | local menu = playdate.getSystemMenu() 35 | local draw_shaded, draw_debug, draw_minimap, draw_minimap_switched = true, false, false, false 36 | 37 | menu:addCheckmarkMenuItem("shading", true, function(value) 38 | draw_shaded = value 39 | end) 40 | menu:addCheckmarkMenuItem("debug map", false, function(value) 41 | draw_debug = value 42 | end) 43 | menu:addCheckmarkMenuItem("mini map", false, function(value) 44 | draw_minimap = value 45 | draw_minimap_switched = true 46 | 47 | end) 48 | 49 | playdate.setMinimumGCTime(2) -- This is necessary to remove frequent stutters 50 | gfx.setColor(gfx.kColorBlack) 51 | playdate.display.setRefreshRate(40) 52 | 53 | local fill_pattern = {{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, -- white 54 | {0xFF, 0xFF, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF, 0xEE}, 55 | {0xFF, 0xBB, 0xFF, 0xEE, 0xFF, 0xBB, 0xFF, 0xEE}, 56 | {0xFF, 0xBB, 0xFF, 0xAA, 0xFF, 0xBB, 0xFF, 0xAA}, 57 | {0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA}, 58 | {0xFF, 0xAA, 0xDD, 0xAA, 0xFF, 0xAA, 0xDD, 0xAA}, 59 | {0x77, 0xAA, 0xDD, 0xAA, 0x77, 0xAA, 0xDD, 0xAA}, 60 | {0x55, 0xAA, 0xDD, 0xAA, 0x55, 0xAA, 0xDD, 0xAA}, 61 | {0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA}, 62 | {0x55, 0xAA, 0x55, 0x88, 0x55, 0xAA, 0x55, 0x88}, 63 | {0x55, 0x22, 0x55, 0x88, 0x55, 0x22, 0x55, 0x88}, 64 | {0x55, 0x0, 0x55, 0x88, 0x55, 0x0, 0x55, 0x88}, 65 | {0x55, 0x0, 0x55, 0x0, 0x55, 0x0, 0x55, 0x0}, 66 | {0x55, 0x0, 0x44, 0x0, 0x55, 0x0, 0x44, 0x0}, 67 | {0x11, 0x0, 0x44, 0x0, 0x11, 0x0, 0x44, 0x0}, 68 | {0x11, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0}, 69 | {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}} -- black 70 | 71 | local map_floor1_flat = 72 | {0,0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1, 73 | 0,0,1,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1, 74 | 0,0,1,0,0,0,0,6,0,0,0,0,0,6,0,0,0,0,0,0,1, 75 | 0,0,1,1,1,1,1,1,1,1,0,1,1,1,1,0,0,0,0,0,1, 76 | 0,0,1,0,0,0,0,0,1,1,0,1,1,0,1,1,0,0,0,1,1, 77 | 0,0,1,0,0,0,0,0,6,0,0,0,1,0,0,1,1,6,1,1,0, 78 | 0,0,1,0,0,0,0,0,1,1,1,1,1,0,0,0,1,0,1,0,0, 79 | 0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,0, 80 | 1,1,1,1,1,6,1,1,1,0,0,0,0,0,0,0,1,0,1,0,0, 81 | 1,0,0,0,1,0,1,0,0,0,0,0,1,1,1,1,1,0,1,1,1, 82 | 1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1, 83 | 1,0,1,1,1,6,1,1,1,0,0,0,1,0,1,1,1,0,1,0,1, 84 | 1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,0,1,0,1, 85 | 1,0,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,1,1,1, 86 | 1,0,6,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,1,0,0, 87 | 1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,0, 88 | 1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0, 89 | 1,0,1,1,1,6,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0, 90 | 1,0,1,0,1,0,1,0,1,1,0,0,0,0,0,1,1,1,1,1,1, 91 | 1,0,0,0,1,0,1,0,0,1,1,1,1,1,1,1,1,1,0,0,1, 92 | 1,0,1,0,1,0,1,0,0,1,1,1,1,0,0,0,0,1,0,4,1, 93 | 1,0,1,1,1,0,1,1,1,1,0,0,0,0,0,0,0,6,0,0,1, 94 | 1,0,0,0,0,0,0,0,0,6,0,0,1,0,0,0,0,1,0,0,1, 95 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} 96 | 97 | local map_floor1 = 98 | {{0,0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1}, 99 | {0,0,1,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1}, 100 | {0,0,1,0,0,0,0,6,0,0,0,0,0,6,0,0,0,0,0,0,1}, 101 | {0,0,1,1,1,1,1,1,1,1,0,1,1,1,1,0,0,0,0,0,1}, 102 | {0,0,1,0,0,0,0,0,1,1,0,1,1,0,1,1,0,0,0,1,1}, 103 | {0,0,1,0,0,0,0,0,6,0,0,0,1,0,0,1,1,6,1,1,0}, 104 | {0,0,1,0,0,0,0,0,1,1,1,1,1,0,0,0,1,0,1,0,0}, 105 | {0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,0}, 106 | {1,1,1,1,1,6,1,1,1,0,0,0,0,0,0,0,1,0,1,0,0}, 107 | {1,0,0,0,1,0,1,0,0,0,0,0,1,1,1,1,1,0,1,1,1}, 108 | {1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1}, 109 | {1,0,1,1,1,6,1,1,1,0,0,0,1,0,1,1,1,0,1,0,1}, 110 | {1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,0,1,0,1}, 111 | {1,0,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,1,1,1}, 112 | {1,0,6,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,1,0,0}, 113 | {1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,0}, 114 | {1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0}, 115 | {1,0,1,1,1,6,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0}, 116 | {1,0,1,0,1,0,1,0,1,1,0,0,0,0,0,1,1,1,1,1,1}, 117 | {1,0,0,0,1,0,1,0,0,1,1,1,1,1,1,1,1,1,0,0,1}, 118 | {1,0,1,0,1,0,1,0,0,1,1,1,1,0,0,0,0,1,0,4,1}, 119 | {1,0,1,1,1,0,1,1,1,1,0,0,0,0,0,0,0,6,0,0,1}, 120 | {1,0,0,0,0,0,0,0,0,6,0,0,1,0,0,0,0,1,0,0,1}, 121 | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}} 122 | 123 | local map = { 124 | 1, 1, 1, 1, 1, 1, 1, 125 | 1, 0, 0, 0, 0, 0, 1, 126 | 1, 0, 1, 1, 0, 1, 1, 127 | 1, 0, 0, 0, 0, 0, 1, 128 | 1, 0, 1, 0, 1, 0, 1, 129 | 1, 0, 1, 0, 1, 0, 1, 130 | 1, 1, 1, 1, 1, 1, 1 131 | } 132 | 133 | local working_map_rows, working_map_columns = nil, nil 134 | local working_map_sprites = {} 135 | 136 | local initialised = false 137 | local map_sprite, player_sprite = nil, nil 138 | local sprite_size = 16 139 | local wall_sprites = table.create(31, 0) 140 | local player_start = {x = 54, y = 24, direction = 90} 141 | local player_speed = 40 142 | local draw_these = table.create(9, 0) 143 | local view = gfx.image.new(400, 240, gfx.kColorBlack) 144 | local background_image = gfx.image.new('Images/background_flat') 145 | local images = {} 146 | local wall_tiles_imagetable = gfx.imagetable.new("Images/wall_tiles-table-16-16") 147 | local gun_shot_sfx = playdate.sound.sample.new("SFX/gun-shot") 148 | local image_map = gfx.image.new(208, 208, gfx.kColorBlack) 149 | local image_minimap = gfx.image.new(90, 90, gfx.kColorClear) 150 | local image_minimap_mask = gfx.image.new("Images/minimap_mask") 151 | local image_minimap_overlay = gfx.image.new("Images/minimap_overlay_v2") 152 | 153 | local function cos_rad(x) 154 | return cos(rad(x)) 155 | end 156 | 157 | local function sin_rad(x) 158 | return sin(rad(x)) 159 | end 160 | 161 | local function isWall(tile_x, tile_y) 162 | -- returns true if working map has a wall at tile_x, tile_y 163 | if working_map[(tile_y - 1) * 7 + tile_x] == 1 then 164 | return true 165 | else 166 | return false 167 | end 168 | end 169 | 170 | local function tileAt(x, y) 171 | -- returns: tileid, column, row 172 | -- or false if outside working map bounds 173 | local column, row = ceil(x/16), ceil(y/16) 174 | if column > 0 and column <= working_map_columns and row > 0 and row <= working_map_rows then 175 | local tileid = (row - 1) * working_map_columns + column 176 | return tileid, column, row 177 | else 178 | return false 179 | end 180 | end 181 | 182 | local function spritesAt(column, row) 183 | -- returns {true, {sprite list} if mini map has a sprite at tile_x, tile_y 184 | local x = (column - 1) * 16 + 8 185 | local y = (row - 1) * 16 + 8 186 | local sprites_at_point = gfx.sprite.querySpritesAtPoint(x, y) 187 | if #sprites_at_point > 0 then 188 | return true, sprites_at_point 189 | end 190 | end 191 | 192 | function makeWorkingMap(columns, rows) 193 | working_map_sprites = {} 194 | local index = 0 195 | for y = 1, rows do 196 | for x = 1, columns do 197 | index += 1 198 | -- put a tile sprite here 199 | local sprite = gfx.sprite.new() 200 | sprite.tileid = index 201 | sprite.row = y 202 | sprite.column = x 203 | sprite.width, sprite.height = sprite_size, sprite_size 204 | sprite.is_wall = false 205 | working_map_sprites[#working_map_sprites + 1] = sprite 206 | end 207 | end 208 | end 209 | 210 | function initialise() 211 | --makeWorkingMap(12, 12) 212 | makeWallImages() 213 | makeWallSprites(map_floor1_flat, 21, 24) 214 | gfx.lockFocus(image_map) 215 | gfx.setColor(gfx.kColorWhite) 216 | for tiley = 1, 24 do 217 | for tilex = 1, 21 do 218 | if map_floor1_flat[(tiley - 1) * 21 + tilex] == 1 then 219 | gfx.fillRect(tilex * 8, tiley * 8, 8, 8) 220 | end 221 | end 222 | end 223 | 224 | player_sprite = makePlayer(player_start.x, player_start.y, player_start.direction) 225 | setUpCamera() 226 | initialised = true 227 | 228 | gfx.sprite.setBackgroundDrawingCallback( 229 | function() 230 | --gfx.setClipRect(10, 10, 380, 220) 231 | view:draw(0, 0) 232 | --gfx.clearClipRect() 233 | end 234 | ) 235 | 236 | image_minimap:addMask() 237 | local mask = image_minimap:getMaskImage() 238 | gfx.lockFocus(mask) 239 | gfx.setColor(gfx.kColorWhite) 240 | gfx.fillCircleAtPoint(43, 43, 40) 241 | gfx.unlockFocus() 242 | end 243 | 244 | function makeWallImages () 245 | 246 | images.walls_noview = {} 247 | images.walls_inview = {} 248 | images.walls_noview.three_n = wall_tiles_imagetable:getImage(12) 249 | images.walls_noview.two_ne = wall_tiles_imagetable:getImage(8) 250 | images.walls_noview.two_ns = wall_tiles_imagetable:getImage(12) 251 | images.walls_noview.one_nes = wall_tiles_imagetable:getImage(2) 252 | images.walls_noview.four_nesw = wall_tiles_imagetable:getImage(1) 253 | 254 | images.walls_inview.three_n = wall_tiles_imagetable:getImage(27) 255 | images.walls_inview.two_ne = wall_tiles_imagetable:getImage(23) 256 | images.walls_inview.two_ns = wall_tiles_imagetable:getImage(27) 257 | images.walls_inview.one_nes = wall_tiles_imagetable:getImage(17) 258 | images.walls_inview.four_nesw = wall_tiles_imagetable:getImage(16) 259 | 260 | end 261 | 262 | function setUpCamera() 263 | 264 | -- calculate smallest number of rays required to detect all tiles in range of camera view_distance 265 | local required_angle = deg(atan(sprite_size/camera.view_distance)) 266 | local camera_rays = floor(camera.fov/required_angle) -- Temp until rays replaced with tree 267 | camera.ray_angles = camera.fov/camera_rays 268 | camera.rays = camera_rays + 1 -- fence segments vs posts 269 | camera.direction = player_sprite.direction 270 | camera.ray_lines = table.create(camera.rays, 0) 271 | print("FOV: " .. camera.fov .. ", " .. camera.rays .. " rays at intervals of " .. floor(camera.ray_angles * 100)/100 .. " degrees") 272 | for i = 1, camera.rays do 273 | local ray_direction = (player_sprite.direction - camera_fov_half) + (camera.ray_angles * (i - 1)) 274 | local ray_end_x = player_sprite.x + camera.view_distance * sin_rad(ray_direction) 275 | local ray_end_y = player_sprite.y - camera.view_distance * cos_rad(ray_direction) 276 | camera.ray_lines[i] = geom.lineSegment.new(player_sprite.x, player_sprite.y, ray_end_x, ray_end_y) 277 | end 278 | end 279 | 280 | 281 | local function getVertices(wall_sprite) 282 | -- Fetch vertices for projecting/drawing 283 | if wall_sprite.x - 8 > player_sprite.x then 284 | if wall_sprite.y - 8 > player_sprite.y then return wall_sprite.view_vertices.nw -- wall is below and right of player 285 | elseif (wall_sprite.y + 8) < player_sprite.y then return wall_sprite.view_vertices.sw -- wall is above and right of player 286 | else return wall_sprite.view_vertices.w end -- wall is directly to right of player 287 | elseif (wall_sprite.x + 8) < player_sprite.x then 288 | if wall_sprite.y - 8 > player_sprite.y then return wall_sprite.view_vertices.ne -- wall is below and left of player 289 | elseif (wall_sprite.y + 8) < player_sprite.y then return wall_sprite.view_vertices.se -- wall is above and left of player 290 | else return wall_sprite.view_vertices.e end -- wall is directly to left of player 291 | elseif (wall_sprite.y - 8) > player_sprite.y then return wall_sprite.view_vertices.n -- wall is directly below player 292 | elseif (wall_sprite.y + 8) < player_sprite.y then return wall_sprite.view_vertices.s -- wall is directly above player 293 | end 294 | end 295 | 296 | local function updateDeltaTime() 297 | -- updates dt (seconds since last frame) 298 | local old_last_time = last_time 299 | last_time = playdate.getCurrentTimeMilliseconds() 300 | dt = (last_time - old_last_time)/1000 301 | end 302 | 303 | function playdate.update() 304 | if initialised == false then initialise() end 305 | 306 | updateDeltaTime() 307 | playdate.timer.updateTimers() 308 | updateView() 309 | gfx.sprite.redrawBackground() 310 | gfx.sprite.update() 311 | 312 | if draw_minimap_switched then 313 | -- local do_draw = draw_minimap and true or false 314 | -- for i = 1, #wall_sprites do 315 | -- wall_sprites[i]:setVisible(do_draw) 316 | -- player_sprite:setVisible(do_draw) 317 | -- end 318 | draw_minimap_switched = false 319 | end 320 | 321 | -- draw camera rays on mini-map 322 | -- draw all rays: for i = 1, camera.rays do 323 | -- draw only left and right rays: for i = 1, camera.rays, (camera.rays -1) do 324 | if draw_minimap then 325 | -- for i = 1, camera.rays, (camera.rays - 1) do 326 | -- gfx.setLineWidth(3) 327 | -- gfx.setColor(gfx.kColorWhite) 328 | -- gfx.drawLine(camera.ray_lines[i]) 329 | -- gfx.setLineWidth(1) 330 | -- gfx.setColor(gfx.kColorBlack) 331 | -- gfx.drawLine(camera.ray_lines[i]) 332 | -- end 333 | gfx.lockFocus(image_minimap) 334 | image_map:draw(-(player_sprite.x-74)/2, -(player_sprite.y-74)/2) 335 | image_minimap:setMaskImage(image_minimap_mask) 336 | image_minimap_overlay:draw(0, 0) 337 | local dirx = 41 * sin(rad(player_sprite.direction)) 338 | local diry = 41 * cos(rad(player_sprite.direction)) 339 | gfx.setColor(gfx.kColorBlack) 340 | gfx.fillCircleAtPoint(45 + dirx, 45 - diry, 4) 341 | gfx.setColor(gfx.kColorWhite) 342 | gfx.drawCircleAtPoint(45 + dirx, 45 - diry, 2) 343 | gfx.unlockFocus() 344 | 345 | image_minimap:draw(310, 4) 346 | end 347 | 348 | playdate.drawFPS(381, 4) 349 | end 350 | 351 | function updateView() 352 | 353 | gfx.pushContext(view) 354 | if draw_shaded then 355 | background_image:draw(0, 0) 356 | else 357 | gfx.fillRect(0, 0, 400, 240) 358 | end 359 | 360 | local screen_polys = {} 361 | local player = geom.point.new(player_sprite.x, player_sprite.y) 362 | local num_draw_these = #draw_these 363 | 364 | for i = 1, num_draw_these do 365 | local points = getVertices(draw_these[i]) 366 | local p = {} 367 | 368 | for i = 1, #points do 369 | p[i] = table.create(0, 4) 370 | p[i].vertex = points[i] 371 | end 372 | 373 | local last_p = #p 374 | if last_p > 0 then -- this skips over all the maths & drawing for objects with no visible vertices 375 | 376 | -- calculate angle to vertex in camera coordinates 377 | for i = 1, last_p do 378 | p[i].delta = player - p[i].vertex 379 | local deltax, deltay = p[i].delta:unpack() 380 | p[i].player_angle = deg(atan2(deltax, -deltay)) +180 381 | p[i].camera_angle = (p[i].player_angle - player_sprite.direction) % 360 382 | if p[i].camera_angle > 180 then p[i].camera_angle -= 360 end 383 | end 384 | 385 | -- remove end point if entire wall is out of view 386 | if last_p == 3 then 387 | if p[1].camera_angle <= camera_fov_half_neg and p[2].camera_angle <= camera_fov_half_neg then 388 | table.remove(p, 1) 389 | last_p -= 1 390 | end 391 | 392 | if p[last_p].camera_angle >= (camera_fov_half) and p[last_p-1].camera_angle >= (camera_fov_half) then 393 | table.remove(p, last_p) 394 | last_p -= 1 395 | end 396 | end 397 | 398 | -- calculate distance between player and vertex as well as 'forward' distance from camera 399 | for i = 1, last_p do 400 | p[i].player_distance = p[i].vertex:distanceToPoint(player) 401 | p[i].camera_distance = p[i].player_distance * cos(rad(p[i].camera_angle)) 402 | end 403 | 404 | local last_point = #p 405 | 406 | -- if wall extends behind camera, shift the vertex to clip the wall 407 | if p[1].camera_distance < sprite_size or p[last_point].camera_distance < sprite_size then -- if walls close enough to extend behind camera 408 | local point = p[1] 409 | local ray_line = point.camera_angle < camera_fov_half_neg and {camera.ray_lines[1], camera_fov_half_neg} 410 | 411 | if ray_line then -- only runs if _left_ vertex outside view 412 | 413 | local x1, y1 = p[2].vertex:unpack() 414 | local x2, y2 = point.vertex:unpack() 415 | local x3, y3, x4, y4 = ray_line[1]:unpack() 416 | local intersects, new_point_x, new_point_y = fast_intersection(x1, y1, x2, y2, x3, y3, x4, y4) 417 | 418 | if intersects then 419 | point.vertex = geom.point.new(new_point_x, new_point_y) 420 | point.delta = point.vertex - player 421 | point.player_distance = point.vertex:distanceToPoint(player) 422 | point.camera_angle = ray_line[2] 423 | point.camera_distance = point.player_distance * cos(rad(point.camera_angle)) 424 | end 425 | end 426 | 427 | local point = p[last_point] 428 | local ray_line = point.camera_angle > camera_fov_half and {camera.ray_lines[camera.rays], camera_fov_half} 429 | 430 | if ray_line then -- only runs if _right_ vertex outside view 431 | local x1, y1 = p[last_point - 1].vertex:unpack() 432 | local x2, y2 = point.vertex:unpack() 433 | local x3, y3, x4, y4 = ray_line[1]:unpack() 434 | local intersects, new_point_x, new_point_y = fast_intersection(x1, y1, x2, y2, x3, y3, x4, y4) 435 | 436 | if intersects then 437 | point.vertex = geom.point.new(new_point_x, new_point_y) 438 | point.delta = point.vertex - player 439 | point.player_distance = point.vertex:distanceToPoint(player) 440 | point.camera_angle = ray_line[2] 441 | point.camera_distance = point.player_distance * cos(rad(point.camera_angle)) 442 | end 443 | end 444 | end 445 | 446 | -- calculate vertex offset from screen centre 447 | for i = 1, last_p do 448 | p[i].offset_x = (p[i].camera_angle/(camera_fov_half)) * (camera_width_half) 449 | p[i].offset_y = (1/p[i].camera_distance) * (camera_height_half) 450 | end 451 | 452 | -- turn points into polygons 453 | for i = 1, last_point - 1 do 454 | screen_polys[#screen_polys+1] = table.create(0, 6) 455 | local poly = screen_polys[#screen_polys] 456 | local next_p = p[i+1] 457 | local p = p[i] 458 | poly.distance = (p.camera_distance + next_p.camera_distance)/2 459 | poly.left_angle = min(p.camera_angle, next_p.camera_angle) 460 | poly.right_angle = max(p.camera_angle, next_p.camera_angle) 461 | 462 | poly.polygon = geom.polygon.new( 463 | 200 + p.offset_x, 120 + p.offset_y*4, 464 | 200 + next_p.offset_x, 120 + next_p.offset_y*4, 465 | 200 + next_p.offset_x, 120 - next_p.offset_y*4, 466 | 200 + p.offset_x, 120 - p.offset_y*4, 467 | 200 + p.offset_x, 120 + p.offset_y*4) 468 | 469 | if draw_debug then 470 | -- draw wall to top-down view 471 | gfx.setColor(gfx.kColorWhite) 472 | gfx.drawLine(340 + p[i].camera_distance * tan(rad(p[i].camera_angle)), 68 - p[i].camera_distance, 473 | 340 + p[i+1].camera_distance * tan(rad(p[i+1].camera_angle)), 68 - p[i+1].camera_distance) 474 | end 475 | end 476 | end 477 | end 478 | 479 | -- Draw polygons 480 | -- sort polygons from nearest to furthest 481 | table.sort(screen_polys, function (k1, k2) return k1.distance < k2.distance end) 482 | -- Draw polygons 483 | local num_screen_polys = #screen_polys 484 | 485 | if draw_shaded == false then 486 | gfx.setColor(gfx.kColorWhite) 487 | --gfx.setImageDrawMode(gfx.kDrawModeNXOR) 488 | for i = num_screen_polys, 1, -1 do 489 | 490 | gfx.drawPolygon(screen_polys[i].polygon) 491 | 492 | end 493 | --gfx.setImageDrawMode(gfx.kDrawModeCopy) 494 | else 495 | for i = num_screen_polys, 1, -1 do 496 | local shade = floor(5 + (screen_polys[i].distance/camera.view_distance) * 11) 497 | if player_sprite.hands.state == "shooting" then 498 | -- apply a lighter pattern 499 | if player_sprite.hands.animation.current.frame == 1 then 500 | gfx.setPattern(fill_pattern[shade - 3]) 501 | elseif player_sprite.hands.animation.current.frame == 2 then 502 | gfx.setPattern(fill_pattern[shade - 2]) 503 | else 504 | gfx.setPattern(fill_pattern[shade]) 505 | end 506 | else 507 | gfx.setPattern(fill_pattern[shade]) 508 | end 509 | gfx.fillPolygon(screen_polys[i].polygon) 510 | end 511 | gfx.setColor(gfx.kColorBlack) 512 | end 513 | 514 | if draw_debug then 515 | gfx.setColor(gfx.kColorWhite) 516 | gfx.drawLine(340, 68, 292, 20) 517 | gfx.drawLine(340, 68, 388, 20) 518 | end 519 | 520 | gfx.setColor(gfx.kColorBlack) 521 | gfx.popContext() 522 | 523 | end 524 | 525 | function makeWallSprites(map, columns, rows) 526 | local map_index = 0 527 | local image_outofview = gfx.image.new(16, 16, gfx.kColorClear) 528 | local image_inview = gfx.image.new(16, 16, gfx.kColorBlack) 529 | gfx.lockFocus(image_inview) 530 | gfx.setColor(gfx.kColorWhite) 531 | gfx.drawRect(1, 1, 14, 14) 532 | gfx.unlockFocus() 533 | gfx.setColor(gfx.kColorBlack) 534 | gfx.lockFocus(image_outofview) 535 | gfx.drawRect(0, 0, 16, 16) 536 | gfx.setColor(gfx.kColorWhite) 537 | gfx.drawRect(1, 1, 14, 14) 538 | gfx.unlockFocus() 539 | 540 | for y = 1, rows do 541 | for x = 1, columns do 542 | map_index += 1 543 | if map[map_index] == 1 then 544 | local s = gfx.sprite.new(16,16) 545 | s.image_inview = image_inview 546 | s.image_noview = image_outofview 547 | s.inview = false 548 | s.wall = true 549 | s.index = map_index 550 | local vertices = {nw = geom.point.new((x-1) * 16, (y-1) * 16), 551 | ne = geom.point.new(x * 16, (y-1) * 16), 552 | se = geom.point.new(x * 16, y * 16), 553 | sw = geom.point.new((x-1) * 16, y * 16)} 554 | s.view_vertices = {} 555 | 556 | local num_walls = 4 557 | 558 | -- cull walls between wall sprites and populate view vertices (8 directions) 559 | if y == 1 or (y > 1 and map[(y - 2) * columns + x] == 1) then s.wall_n = true num_walls -=1 else s.wall_n = false end 560 | if y == 24 or (y < 24 and map[y * columns + x] == 1) then s.wall_s = true num_walls -=1 else s.wall_s = false end 561 | if x == 1 or (x > 1 and map[(y - 1) * columns + x - 1] == 1) then s.wall_w = true num_walls -=1 else s.wall_w = false end 562 | if x == 24 or (x < 24 and map[(y - 1) * columns + x + 1] == 1) then s.wall_e = true num_walls -=1 else s.wall_e = false end 563 | 564 | -- if num_walls == 4 then 565 | -- s.image_noview = wall_tiles_imagetable:getImage(1) 566 | -- s.image_inview = wall_tiles_imagetable:getImage(16) 567 | -- elseif num_walls == 3 then 568 | -- if s.wall_n then 569 | -- s.image_noview = wall_tiles_imagetable:getImage(2) 570 | -- s.image_inview = wall_tiles_imagetable:getImage(17) 571 | -- elseif s.wall_e then 572 | -- s.image_noview = wall_tiles_imagetable:getImage(3) 573 | -- s.image_inview = wall_tiles_imagetable:getImage(18) 574 | -- elseif s.wall_s then 575 | -- s.image_noview = wall_tiles_imagetable:getImage(4) 576 | -- s.image_inview = wall_tiles_imagetable:getImage(19) 577 | -- elseif s.wall_w then 578 | -- s.image_noview = wall_tiles_imagetable:getImage(5) 579 | -- s.image_inview = wall_tiles_imagetable:getImage(20) 580 | -- end 581 | -- elseif num_walls == 2 then 582 | -- if s.wall_s and s.wall_w then 583 | -- s.image_noview = wall_tiles_imagetable:getImage(6) 584 | -- s.image_inview = wall_tiles_imagetable:getImage(21) 585 | -- elseif s.wall_w and s.wall_n then 586 | -- s.image_noview = wall_tiles_imagetable:getImage(7) 587 | -- s.image_inview = wall_tiles_imagetable:getImage(22) 588 | -- elseif s.wall_n and s.wall_e then 589 | -- s.image_noview = wall_tiles_imagetable:getImage(8) 590 | -- s.image_inview = wall_tiles_imagetable:getImage(23) 591 | -- elseif s.wall_e and s.wall_s then 592 | -- s.image_noview = wall_tiles_imagetable:getImage(9) 593 | -- s.image_inview = wall_tiles_imagetable:getImage(24) 594 | -- elseif s.wall_n and s.wall_s then 595 | -- s.image_noview = wall_tiles_imagetable:getImage(10) 596 | -- s.image_inview = wall_tiles_imagetable:getImage(25) 597 | -- elseif s.wall_e and s.wall_w then 598 | -- s.image_noview = wall_tiles_imagetable:getImage(11) 599 | -- s.image_inview = wall_tiles_imagetable:getImage(26) 600 | -- end 601 | -- elseif num_walls == 1 then 602 | -- if s.wall_e and s.wall_s and s.wall_w then 603 | -- s.image_noview = wall_tiles_imagetable:getImage(12) 604 | -- s.image_inview = wall_tiles_imagetable:getImage(27) 605 | -- elseif s.wall_s and s.wall_w and s.wall_n then 606 | -- s.image_noview = wall_tiles_imagetable:getImage(13) 607 | -- s.image_inview = wall_tiles_imagetable:getImage(28) 608 | -- elseif s.wall_w and s.wall_n and s.wall_e then 609 | -- s.image_noview = wall_tiles_imagetable:getImage(14) 610 | -- s.image_inview = wall_tiles_imagetable:getImage(29) 611 | -- elseif s.wall_n and s.wall_e and s.wall_s then 612 | -- s.image_noview = wall_tiles_imagetable:getImage(15) 613 | -- s.image_inview = wall_tiles_imagetable:getImage(30) 614 | -- end 615 | -- end 616 | 617 | if not (s.wall_n and s.wall_s and s.wall_e and s.wall_w) then 618 | 619 | -- when wall is below and right of player, draw left and top sides 620 | if s.wall_n and s.wall_w then s.view_vertices.nw = table.create(2, 0) 621 | elseif s.wall_n then s.view_vertices.nw = {vertices.nw, vertices.sw} 622 | elseif s.wall_w then s.view_vertices.nw = {vertices.ne, vertices.nw} 623 | else s.view_vertices.nw = {vertices.ne, vertices.nw, vertices.sw} 624 | end 625 | 626 | -- when wall is above and right of player, draw left and bottom sides 627 | if s.wall_w and s.wall_s then s.view_vertices.sw = table.create(2, 0) 628 | elseif s.wall_w then s.view_vertices.sw = {vertices.sw, vertices.se} 629 | elseif s.wall_s then s.view_vertices.sw = {vertices.nw, vertices.sw} 630 | else s.view_vertices.sw = {vertices.nw, vertices.sw, vertices.se} 631 | end 632 | 633 | -- when wall is below and left of player, draw right and top sides 634 | if s.wall_n and s.wall_e then s.view_vertices.ne = table.create(2, 0) 635 | elseif s.wall_n then s.view_vertices.ne = {vertices.se, vertices.ne} 636 | elseif s.wall_e then s.view_vertices.ne = {vertices.ne, vertices.nw} 637 | else s.view_vertices.ne = {vertices.se, vertices.ne, vertices.nw} 638 | end 639 | 640 | -- when wall is above and left of player, draw right and bottom sides 641 | if s.wall_e and s.wall_s then s.view_vertices.se = table.create(2, 0) 642 | elseif s.wall_e then s.view_vertices.se = {vertices.sw, vertices.se} 643 | elseif s.wall_s then s.view_vertices.se = {vertices.se, vertices.ne} 644 | else s.view_vertices.se = {vertices.sw, vertices.se, vertices.ne} 645 | end 646 | 647 | -- when wall is directly below player, only draw the top side 648 | if s.wall_n then s.view_vertices.n = table.create(2, 0) 649 | else s.view_vertices.n = {vertices.ne, vertices.nw} 650 | end 651 | 652 | -- when wall is directly above player, only draw the bottom side 653 | if s.wall_s then s.view_vertices.s = table.create(2, 0) 654 | else s.view_vertices.s = {vertices.sw, vertices.se} 655 | end 656 | 657 | -- when wall is directly to right of player, only draw the left side 658 | if s.wall_w then s.view_vertices.w = table.create(2, 0) 659 | else s.view_vertices.w = {vertices.nw, vertices.sw} 660 | end 661 | 662 | -- when wall is directly to left of player, only draw the right side 663 | if s.wall_e then s.view_vertices.e = table.create(2, 0) 664 | else s.view_vertices.e = {vertices.se, vertices.ne} 665 | end 666 | 667 | s:setCollideRect(0, 0, 16, 16) 668 | 669 | function s.update() 670 | if s.inview == true and s:getImage() ~= s.image_inview then 671 | s:setImage(s.image_inview) 672 | elseif s.inview == false and s:getImage() ~= s.image_noview then 673 | s:setImage(s.image_noview) 674 | s.inview = false 675 | else 676 | s.inview = false 677 | end 678 | end 679 | 680 | s:add() 681 | s:moveTo((x-1) * 16+8, (y-1) * 16+8) 682 | s:setVisible(false) 683 | 684 | wall_sprites[#wall_sprites + 1] = s 685 | end 686 | end 687 | end 688 | 689 | end 690 | end 691 | 692 | function makePlayer(x_pos, y_pos, direction) 693 | local hands = gfx.sprite.new() 694 | hands.image = gfx.image.new(176, 160, gfx.kColorClear) 695 | hands.state = "idle" 696 | hands.imagetable = gfx.imagetable.new('Images/hands') 697 | hands.animation = { shoot = gfx.animation.loop.new(100, animation_grid(hands.imagetable, {1, 2, 3}), false), 698 | reload = gfx.animation.loop.new(100, animation_grid(hands.imagetable, {4, 5, 6, 7, 8}), false), 699 | idle = gfx.animation.loop.new(100, animation_grid(hands.imagetable, {1}), true)} 700 | hands.animation.current = hands.animation.idle 701 | function hands:update() 702 | if hands.state == "idle" then 703 | if playdate.buttonIsPressed(playdate.kButtonA) then 704 | gun_shot_sfx:play() 705 | hands.state = "shooting" 706 | hands.animation.current = gfx.animation.loop.new(100, animation_grid(hands.imagetable, {2, 3, 1}), false) 707 | end 708 | elseif hands.animation.current.frame == 3 then 709 | hands.state = "idle" 710 | hands.animation.current = hands.animation.idle 711 | end 712 | gfx.lockFocus(hands.image) 713 | gfx.setColor(gfx.kColorClear) 714 | gfx.fillRect(0, 0, 176, 160) 715 | hands.animation.current:draw(0, 0) 716 | gfx.unlockFocus() 717 | hands:setImage(hands.image) 718 | end 719 | hands:add() 720 | hands:moveTo(240, 160) 721 | 722 | local image = gfx.image.new(6, 6) 723 | gfx.lockFocus(image) 724 | gfx.fillCircleAtPoint(3, 3, 3) 725 | gfx.setColor(gfx.kColorWhite) 726 | gfx.fillCircleAtPoint(3, 30, 2) 727 | gfx.unlockFocus() 728 | 729 | local s = gfx.sprite.new(image) 730 | s.hands = hands 731 | s.moved = false 732 | s.direction = direction 733 | s:setCollideRect(0, 0, 6, 6) 734 | s:setCenter(0.5, 0.5) 735 | s.collisionResponse = gfx.sprite.kCollisionTypeSlide 736 | s.rotate_transform = playdate.geometry.affineTransform.new() 737 | s.sin_dir = sin_rad(s.direction) 738 | s.cos_dir = cos_rad(s.direction) 739 | s.view_left = geom.lineSegment.new(x_pos, y_pos, x_pos + sin_rad(s.direction - camera_fov_half), y_pos - cos_rad(s.direction - camera_fov_half)) 740 | s.view_right = geom.lineSegment.new(x_pos, y_pos, x_pos + sin_rad(s.direction + camera_fov_half), y_pos - cos_rad(s.direction + camera_fov_half)) 741 | function s:update() 742 | 743 | local movex, movey = 0, 0 744 | if playdate.buttonIsPressed(playdate.kButtonRight) then 745 | if playdate.buttonIsPressed(playdate.kButtonB) then 746 | -- strafe right 747 | movex = s.cos_dir 748 | movey = -s.sin_dir 749 | s.moved = true 750 | else 751 | -- turn right 752 | s.direction += 4 753 | s.rotate_transform:rotate(4, s.x, s.y) 754 | if s.direction > 360 then s.direction -= 360 end 755 | s.moved = true 756 | end 757 | end 758 | if playdate.buttonIsPressed(playdate.kButtonLeft) then 759 | if playdate.buttonIsPressed(playdate.kButtonB) then 760 | -- strafe left 761 | movex = -s.cos_dir 762 | movey = s.sin_dir 763 | s.moved = true 764 | else 765 | -- turn left 766 | s.direction -= 4 767 | s.rotate_transform:rotate(-4, s.x, s.y) 768 | if s.direction < 0 then s.direction += 360 end 769 | s.moved = true 770 | end 771 | end 772 | if playdate.buttonIsPressed(playdate.kButtonUp) then 773 | movex = s.sin_dir 774 | movey = s.cos_dir 775 | s.moved = true 776 | end 777 | if playdate.buttonIsPressed(playdate.kButtonDown) then 778 | movex = -s.sin_dir 779 | movey = -s.cos_dir 780 | s.moved = true 781 | end 782 | 783 | if s.moved then 784 | 785 | local actualX, actualY, collisions = s:moveWithCollisions(s.x + (movex * dt * player_speed), s.y - (movey * dt * player_speed)) 786 | for i = 1, #camera.ray_lines do 787 | camera.ray_lines[i]:offset(-(camera.ray_lines[i].x - actualX), -(camera.ray_lines[i].y - actualY)) 788 | camera.ray_lines[i] = s.rotate_transform:transformedLineSegment(camera.ray_lines[i]) 789 | end 790 | 791 | s.view_left = camera.ray_lines[1] 792 | s.view_right = camera.ray_lines[#camera.ray_lines] 793 | 794 | s.sin_dir = sin_rad(s.direction) 795 | s.cos_dir = cos_rad(s.direction) 796 | 797 | s.moved = false 798 | s.rotate_transform:reset() 799 | end 800 | 801 | s:raytrace() 802 | end 803 | 804 | function s:raytrace() 805 | draw_these = table.create(9, 0) 806 | -- trace rays 807 | for i = 1, camera.rays do 808 | ray_hits = gfx.sprite.querySpritesAlongLine(camera.ray_lines[i]) 809 | for i = 1, min(#ray_hits, 4) do 810 | ray_hits[i].inview = true 811 | end 812 | end 813 | for i = 1, #wall_sprites do 814 | if wall_sprites[i].inview then 815 | draw_these[#draw_these + 1] = wall_sprites[i] 816 | end 817 | end 818 | end 819 | 820 | s:setVisible(false) 821 | s:add() 822 | s:moveTo(x_pos, y_pos) 823 | 824 | return s 825 | 826 | end 827 | 828 | function animation_grid(imagetable, sequence) 829 | local temp_imagetable = gfx.imagetable.new(#sequence) 830 | for i, v in ipairs(sequence) do 831 | temp_imagetable:setImage(i, imagetable:getImage(v)) 832 | end 833 | return temp_imagetable 834 | end --------------------------------------------------------------------------------