20 | This is a basic raycasting engine rendered on an HTML5 canvas.
21 | See readme.txt for more information.
22 |
23 | Code is available on GitHub
24 | Old, simpler, version with grid-based engine still available here.
25 |
26 | Runs fastest in Chrome or IE9 and pretty slow in FF. Not yet tested in other browsers.
27 | If you don't see anything, you should update your browser...
28 |
29 | Usage: W/S = walk, Q/E = turn, A/D = strafe.
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 |
59 |
60 |
61 |
66 |
67 |
68 |
69 |
73 |
74 |
* = contains unfinished features
75 |
76 |
77 |
78 |
‹
79 |
←
80 |
↑
81 |
↓
82 |
→
83 |
›
84 |
85 |
86 |
89 |
90 |
91 |
92 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/js/raycaster.objects.js:
--------------------------------------------------------------------------------
1 | /*
2 | // Namespace: Raycaster.Objects
3 | // Description: Instances of global objects and variables used throughout the application
4 | */
5 | Raycaster.Objects =
6 | {
7 | // Defines player parameters
8 | player:
9 | {
10 | x: Raycaster.Constants.playerStartPos.x,
11 | y: Raycaster.Constants.playerStartPos.y,
12 | z: Raycaster.Constants.playerStartPos.z,
13 | angle: new Raycaster.Classes.Angle(Raycaster.Constants.playerStartAngle),
14 | height: 48,
15 | width: 0
16 | },
17 |
18 | // Settings checkboxes states
19 | settings:
20 | {
21 | renderTextures: function() {
22 | return document.getElementById("chkTextures").checked;
23 | },
24 | renderLighting: function() {
25 | return document.getElementById("chkLighting").checked;
26 | },
27 | renderMiniMap: function() {
28 | return document.getElementById("chkMiniMap").checked;
29 | },
30 | renderSky: function() {
31 | return document.getElementById("chkSky").checked;
32 | },
33 | renderFloor: function() {
34 | return false; //document.getElementById("chkFloor").checked;
35 | },
36 | renderSprites: function() {
37 | return document.getElementById("chkSprites").checked;
38 | },
39 | selectedLevel: function() {
40 | var selected = 0;
41 |
42 | if (location.hash) {
43 | var settings = location.hash.split("#")[1];
44 | var index = parseInt(settings.split(",")[0]);
45 |
46 | if (index) {
47 | selected = index;
48 | }
49 | }
50 |
51 | return selected;
52 | },
53 | selectedResolution: function() {
54 | var selected = { w: 640, h: 480 };
55 |
56 | if (location.hash) {
57 | var settings = location.hash.split("#")[1];
58 | var res = parseInt(settings.split(",")[1]);
59 |
60 | if (res == 320) {
61 | selected = { w: 320, h: 240 };
62 | document.getElementById("ddlSize").selectedIndex = 1;
63 | }
64 | else {
65 | document.getElementById("ddlSize").selectedIndex = 0;
66 | }
67 | }
68 |
69 | return selected;
70 | }
71 | },
72 |
73 | // Definition of keyboard buttons
74 | keys: {
75 | arrowLeft: Raycaster.Classes.KeyButton(37),
76 | arrowUp: Raycaster.Classes.KeyButton(38),
77 | arrowRight: Raycaster.Classes.KeyButton(39),
78 | arrowDown: Raycaster.Classes.KeyButton(40),
79 | lessThan: Raycaster.Classes.KeyButton(188),
80 | greaterThan: Raycaster.Classes.KeyButton(190),
81 | esc: Raycaster.Classes.KeyButton(27),
82 | shift: Raycaster.Classes.KeyButton(16),
83 | charR: Raycaster.Classes.KeyButton(82),
84 | charA: Raycaster.Classes.KeyButton(65),
85 | charZ: Raycaster.Classes.KeyButton(90),
86 | charQ: Raycaster.Classes.KeyButton(81),
87 | charW: Raycaster.Classes.KeyButton(87),
88 | charE: Raycaster.Classes.KeyButton(69),
89 | charS: Raycaster.Classes.KeyButton(83),
90 | charD: Raycaster.Classes.KeyButton(68),
91 | charX: Raycaster.Classes.KeyButton(88)
92 | },
93 |
94 | context: null, // Reference to the canvas context
95 | bufferctx: null,
96 | gameloopInterval: null, // Reference to the interval that triggers the update functions
97 | redrawScreen: true, // Wether it is necessary to redraw the scene
98 | textures: null, // Array with texture Image objects
99 | sprites: null, // Array with sprite Image objects
100 | skyImage: new Image(), // Holds the image that is used for the sky background
101 |
102 | /*
103 | // Method: loadResources
104 | // Description: Loads the texture and sprite images in memory
105 | // Parameters: -
106 | // Returns: -
107 | */
108 | loadResources: function()
109 | {
110 | //need this when creating images in a different canvas
111 | //netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
112 |
113 | // Load texture images
114 | Raycaster.Objects.textures = new Array();
115 | for (var i = 0; i < Raycaster.Constants.texturesFiles.length; i++) {
116 | Raycaster.Objects.textures[i] = new Image();
117 | Raycaster.Objects.textures[i].src = Raycaster.Constants.texturesFiles[i];
118 | Raycaster.Objects.textures[i].onload = function() {
119 | Raycaster.Objects.redrawScreen = true;
120 | };
121 | }
122 |
123 | // Load sprite images
124 | Raycaster.Objects.sprites = new Array();
125 | for (var i = 0; i < Raycaster.Constants.spriteFiles.length; i++) {
126 | Raycaster.Objects.sprites[i] = new Image();
127 | Raycaster.Objects.sprites[i].src = Raycaster.Constants.spriteFiles[i];
128 | }
129 |
130 | // Load sky background image
131 | Raycaster.Objects.skyImage.src = Raycaster.Constants.skyImage;
132 | Raycaster.Objects.skyImage.onload = function() {
133 | Raycaster.Objects.redrawScreen = true;
134 | };
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/js/raycaster.classes.js:
--------------------------------------------------------------------------------
1 | /*
2 | // Namespace: Raycaster.Classes
3 | // Description: Definition of classes
4 | */
5 | Raycaster.Classes =
6 | {
7 | Point: function(x, y) {
8 | return {
9 | x: x,
10 | y: y
11 | };
12 | },
13 |
14 | Vector: function(x1, y1, x2, y2) {
15 | return {
16 | x1: x1,
17 | y1: y1,
18 | x2: x2,
19 | y2: y2
20 | };
21 | },
22 |
23 | /*
24 | // Class: Raycaster.Classes.Sprite
25 | // Description: Parameters that define a sprite in the game world
26 | */
27 | Sprite: function(x, y, z, id, yoff) {
28 | return {
29 | x: x, // x,y location of sprite in the game world
30 | y: y,
31 | z: z,
32 | yoff: yoff, // Offset for Y coordinate, to make it possible to place things on the floor or ceiling
33 | id: id // index of sprite image in Constants.spriteFiles array
34 | };
35 | },
36 |
37 | /*
38 | // Class: Raycaster.Classes.Wall
39 | // Description: Parameters that define a wall in the game world
40 | */
41 | Wall: function(x1, y1, x2, y2, z1, z2, h1, h2, textureId) {
42 | return {
43 | x1: x1, // x1, y1: wall start point
44 | y1: y1,
45 | x2: x2, // x2, y2: wall end point
46 | y2: y2,
47 | z1: z1, // wall elevation at start
48 | z2: z2, // wall elevation at end
49 | h1: h1, // wall height at start
50 | h2: h2, // wall height at end
51 | textureId: textureId, // id (index in Constants.texturesFiles array) of texture to use on this wall
52 | maxHeight: (h1 > h2 ? h1: h2)
53 | };
54 | },
55 |
56 | /*
57 | // Class: Raycaster.Classes.Elevation
58 | // Description: Defines an elevation in the floor
59 | */
60 | Elevation: function(height, area) {
61 | return {
62 | area: area, // One Vector defining the size of the elevated area.
63 | // Currently limits to elevations being squared and angled straight
64 | height: height // Height of the elevation
65 | };
66 | },
67 |
68 | /*
69 | // Class: Raycaster.Classes.VSliceDrawParams
70 | // Description: Drawing parameters for a vertical slice (scanline) of an object
71 | */
72 | VSliceDrawParams: function() {
73 | return {
74 | dy1: 0, // Destination start Y coord
75 | dy2: 0, // Destination end Y coord
76 | sy1: 0, // Source image start Y coord
77 | sy2: 0, // Source image end Y coord
78 | texture: null // Image object containing the texture or sprite to draw
79 | };
80 | },
81 |
82 | Intersection: function() {
83 | return {
84 | x: 0, // X coordinate of this intersection
85 | y: 0, // Y coordinate of this intersection
86 | distance: 0, // Distance to the intersection
87 | resourceId: 0, // index of texture or sprite image in Objects namespace
88 | levelObjectId: 0, // index of texture or sprite in Objects.Level namespace
89 | textureX: 0, // X coordinate of the texture scanline to draw
90 | isSprite: false, // true if intersection if for a sprite, otherwise its for a wall
91 | drawParams: null // VSliceDrawParams object for this intersection
92 | };
93 | },
94 |
95 | KeyButton: function(code) {
96 | return {
97 | code: code,
98 | pressed: false
99 | };
100 | },
101 |
102 | Angle: function(degrees) {
103 |
104 | var self = this;
105 | this.degrees = degrees;
106 | this.radians = 0;
107 |
108 | // Set the value of this angle
109 | // Corrects negative values or values greater than 360 degrees
110 | this.setValue = function(v) {
111 | self.degrees = Number(v);
112 |
113 | if (self.degrees >= 360) {
114 | self.degrees -= 360;
115 | }
116 | if (self.degrees < 0) {
117 | self.degrees += 360;
118 | }
119 |
120 | self.radians = self.toRadians();
121 | };
122 |
123 | // Converts the angle from degrees to radians
124 | this.toRadians = function() {
125 | if (self.degrees == 90) {
126 | return Math.PI / 2;
127 | }
128 | else if (self.degrees == 270) {
129 | return 3 * Math.PI / 2;
130 | }
131 | return self.degrees * (Math.PI / 180);
132 | };
133 |
134 | // Turn the angle with n degrees
135 | this.turn = function(degrees) {
136 | self.setValue(self.degrees + degrees);
137 | };
138 |
139 | // Determine to which quadrant of a circle the angle is facing
140 | this.getQuadrant = function() {
141 | var rounded = ~~ (0.5 + self.degrees);
142 |
143 | if ((rounded >= 0 || rounded == 360) && rounded < 90) {
144 | return 1;
145 | }
146 | if (rounded >= 90 && rounded < 180) {
147 | return 2;
148 | }
149 | if (rounded >= 180 && rounded < 270) {
150 | return 3;
151 | }
152 | if (rounded >= 270 && rounded < 360) {
153 | return 4;
154 | }
155 | };
156 |
157 | this.setValue(degrees);
158 | }
159 | };
--------------------------------------------------------------------------------
/js/raycaster.objects.level.js:
--------------------------------------------------------------------------------
1 | // Hard-coded level definition
2 | var demoLevels = new Array();
3 |
4 | demoLevels[0] = {
5 | walls: [
6 | // Walls surrounding the level
7 | Raycaster.Classes.Wall(100, 10, 700, 10, 0, 0, 200, 200, 1),
8 | Raycaster.Classes.Wall(10, 100, 10, 700, 0, 0, 200, 200, 1),
9 | Raycaster.Classes.Wall(10, 100, 100, 10, 0, 0, 200, 200, 1),
10 | Raycaster.Classes.Wall(790, 100, 790, 700, 0, 0, 200, 200, 1),
11 | Raycaster.Classes.Wall(700, 10, 790, 100, 0, 0, 200, 200, 1),
12 | Raycaster.Classes.Wall(100, 790, 700, 790, 0, 0, 200, 200, 1),
13 | Raycaster.Classes.Wall(790, 700, 700, 790, 0, 0, 200, 200, 1),
14 | Raycaster.Classes.Wall(10, 700, 100, 790, 0, 0, 200, 200, 1),
15 |
16 | // Walls in the middle
17 | Raycaster.Classes.Wall(340, 300, 380, 320, 0, 0, 64, 64, 0),
18 | Raycaster.Classes.Wall(380, 320, 420, 360, 0, 0, 64, 64, 0),
19 | Raycaster.Classes.Wall(420, 360, 420, 424, 0, 0, 64, 64, 2),
20 | Raycaster.Classes.Wall(420, 424, 400, 440, 0, 0, 64, 64, 0),
21 | Raycaster.Classes.Wall(400, 440, 400, 470, 0, 0, 64, 64, 0),
22 | Raycaster.Classes.Wall(400, 470, 430, 510, 0, 0, 64, 64, 0),
23 | Raycaster.Classes.Wall(430, 510, 500, 530, 0, 0, 64, 64, 0),
24 | Raycaster.Classes.Wall(500, 530, 560, 530, 0, 0, 64, 64, 0),
25 | Raycaster.Classes.Wall(560, 530, 580, 500, 0, 0, 64, 64, 0),
26 | Raycaster.Classes.Wall(580, 500, 560, 280, 0, 0, 64, 64, 0),
27 | Raycaster.Classes.Wall(560, 280, 500, 220, 0, 0, 64, 64, 0),
28 | Raycaster.Classes.Wall(500, 220, 340, 190, 0, 0, 64, 64, 0),
29 | Raycaster.Classes.Wall(340, 190, 340, 300, 0, 0, 64, 64, 0),
30 |
31 | Raycaster.Classes.Wall(240, 10, 300, 84, 0, 0, 128, 128, 1),
32 | Raycaster.Classes.Wall(300, 84, 410, 84, 0, 0, 128, 128, 1),
33 | Raycaster.Classes.Wall(410, 84, 470, 10, 0, 0, 128, 128, 1)
34 | ],
35 |
36 | sprites: [
37 | Raycaster.Classes.Sprite(355, 171, 0, 2),
38 | Raycaster.Classes.Sprite(355, 137, 0, 2),
39 | Raycaster.Classes.Sprite(355, 103, 0, 2),
40 | Raycaster.Classes.Sprite(320, 230, 0, 0),
41 | Raycaster.Classes.Sprite(320, 247, 30, 0),
42 | Raycaster.Classes.Sprite(320, 265, 0, 0)
43 | ],
44 |
45 | // Define floor elevations
46 | // Make sure the vectors appear in the correct order (connecting lines) and the area is closed
47 | // Must be a straight square!!
48 | elevations: [
49 | ],
50 |
51 | floorTextureId: 4,
52 |
53 | init: function() {
54 | Raycaster.Objects.player.angle.setValue(340);
55 | }
56 | };
57 |
58 | demoLevels[1] =
59 | {
60 | walls: [
61 | // Walls surrounding the level
62 | Raycaster.Classes.Wall(100, 10, 700, 10, 0, 0, 200, 200, 1),
63 | Raycaster.Classes.Wall(10, 100, 10, 700, 0, 0, 200, 200, 1),
64 | Raycaster.Classes.Wall(10, 100, 100, 10, 0, 0, 200, 200, 1),
65 | Raycaster.Classes.Wall(790, 100, 790, 700, 0, 0, 200, 200, 1),
66 | Raycaster.Classes.Wall(700, 10, 790, 100, 0, 0, 200, 200, 1),
67 | Raycaster.Classes.Wall(100, 790, 700, 790, 0, 0, 200, 200, 1),
68 | Raycaster.Classes.Wall(790, 700, 700, 790, 0, 0, 200, 200, 1),
69 | Raycaster.Classes.Wall(10, 700, 100, 790, 0, 0, 200, 200, 1),
70 |
71 | // Stairways
72 | Raycaster.Classes.Wall(59, 240, 10, 240, 0, 0, 200, 200, 0),
73 | Raycaster.Classes.Wall(59, 240, 59, 600, 0, 0, 200, 200, 0),
74 | Raycaster.Classes.Wall(59, 601, 400, 601, 0, 0, 200, 200, 0),
75 | Raycaster.Classes.Wall(400, 601, 400, 650, 0, 0, 200, 200, 0),
76 | Raycaster.Classes.Wall(399, 650, 10, 650, 0, 0, 200, 200, 0),
77 | Raycaster.Classes.Wall(181, 240, 180, 480, 0, 0, 200, 200, 0),
78 | Raycaster.Classes.Wall(181, 480, 400, 480, 0, 0, 200, 200, 0),
79 | Raycaster.Classes.Wall(400, 481, 400, 600, 0, 0, 45, 45, 0),
80 | Raycaster.Classes.Wall(181, 240, 400, 480, 0, 0, 200, 200, 0)
81 | ],
82 |
83 | sprites: [
84 | ],
85 |
86 | // Define floor elevations
87 | // Make sure the vectors appear in the correct order (connecting lines) and the area is closed
88 | // Must be a straight square!!
89 | elevations: [
90 | // Elevate the floor inside the square
91 | Raycaster.Classes.Elevation(45, Raycaster.Classes.Vector(180, 480, 400, 600))
92 | ],
93 |
94 | floorTextureId: 4,
95 |
96 | // Generates a stairway in the level
97 | init: function() {
98 | Raycaster.Objects.player.angle.setValue(280);
99 |
100 | var level = Raycaster.Objects.Level,
101 | classes = Raycaster.Classes,
102 | stairwidth = 120,
103 | stepwidth = 120,
104 | startX = 60,
105 | startY = 240;
106 |
107 | // Generate walls for stairway
108 | for (var i = 0; i < 3; i++) {
109 | var stepheight = 15,
110 | z = i * 15,
111 | y = startY + i * stepwidth;
112 |
113 | level.walls[level.walls.length] = classes.Wall(startX, y, startX + stairwidth, y, z, z, stepheight, stepheight, 0);
114 | if (i < 2) {
115 | level.walls[level.walls.length] = classes.Wall(startX + stairwidth, y, startX + stairwidth, y + stepwidth, z, z, stepheight, stepheight, 0);
116 | level.walls[level.walls.length] = classes.Wall(startX + stairwidth, y + stepwidth, startX, y + stepwidth, z, z, stepheight, stepheight, 0);
117 | level.walls[level.walls.length] = classes.Wall(startX, y + stepwidth, startX, y, z, z, stepheight, stepheight, 0);
118 | }
119 | level.elevations[level.elevations.length] = Raycaster.Classes.Elevation(z + stepheight, Raycaster.Classes.Vector(startX, y, startX + stairwidth, y + stepwidth));
120 | }
121 | }
122 | };
123 |
124 | demoLevels[2] =
125 | {
126 | walls: [
127 | // Walls surrounding the level
128 | Raycaster.Classes.Wall(100, 10, 700, 10, 0, 0, 200, 200, 1),
129 | Raycaster.Classes.Wall(10, 100, 10, 700, 0, 0, 200, 200, 1),
130 | Raycaster.Classes.Wall(10, 100, 100, 10, 0, 0, 200, 200, 1),
131 | Raycaster.Classes.Wall(790, 100, 790, 700, 0, 0, 200, 200, 1),
132 | Raycaster.Classes.Wall(700, 10, 790, 100, 0, 0, 200, 200, 1),
133 | Raycaster.Classes.Wall(100, 790, 700, 790, 0, 0, 200, 200, 1),
134 | Raycaster.Classes.Wall(790, 700, 700, 790, 0, 0, 200, 200, 1),
135 | Raycaster.Classes.Wall(10, 700, 100, 790, 0, 0, 200, 200, 1),
136 |
137 | // Angled and elevated walls
138 | Raycaster.Classes.Wall(300, 10, 300, 90, 0, 0, 120, 120, 0),
139 | Raycaster.Classes.Wall(300, 90, 300, 130, 40, 120, 80, 80, 0),
140 | Raycaster.Classes.Wall(300, 130, 300, 170, 120, 120, 80, 80, 0),
141 | Raycaster.Classes.Wall(300, 170, 300, 210, 120, 40, 80, 80, 0),
142 | Raycaster.Classes.Wall(300, 210, 300, 290, 0, 0, 120, 120, 0)
143 | ],
144 |
145 | sprites: [
146 | Raycaster.Classes.Sprite(455, 171, 0, 2),
147 | Raycaster.Classes.Sprite(455, 137, 0, 2),
148 | Raycaster.Classes.Sprite(455, 103, 0, 2),
149 | Raycaster.Classes.Sprite(420, 230, 0, 0),
150 | Raycaster.Classes.Sprite(420, 247, 30, 0),
151 | Raycaster.Classes.Sprite(420, 265, 0, 0)
152 | ],
153 |
154 | elevations: [
155 | ],
156 |
157 | floorTextureId: 4,
158 |
159 | // Generates a stairway in the level
160 | init: function() {
161 | Raycaster.Objects.player.angle.setValue(4);
162 | }
163 | };
164 |
165 | Raycaster.Objects.Level = demoLevels[0];
--------------------------------------------------------------------------------
/js/raycaster.movement.js:
--------------------------------------------------------------------------------
1 | /*
2 | // Namespace: Raycaster.Drawing
3 | // Description: Contains all movement related functions
4 | //
5 | // Public methods: init()
6 | // update()
7 | */
8 | Raycaster.Movement = function()
9 | {
10 | var player = Raycaster.Objects.player;
11 |
12 | /****************** / Private methods / *****************/
13 | // Returns closest intersection (wall or sprite) at specified angle.
14 | // This is used for collision detection.
15 | var findIntersection = function(angle)
16 | {
17 | if (Raycaster.Constants.noClipping) {
18 | return false;
19 | }
20 |
21 | var objects = Raycaster.Raycasting.findObjects(angle);
22 |
23 | if (objects.length > 0) {
24 | return objects[objects.length - 1];
25 | }
26 |
27 | return false;
28 | }
29 |
30 | // Make the player turn by increasing its viewing angle
31 | var turn = function(angle)
32 | {
33 | player.angle.turn(angle);
34 | Raycaster.engine.redraw();
35 | };
36 |
37 | // Make the player walk forward or backwards
38 | var walk = function(forward)
39 | {
40 | step = forward ? Raycaster.Constants.movementStep : -Raycaster.Constants.movementStep;
41 |
42 | var delta = Raycaster.Raycasting.getDeltaXY(player.angle, step);
43 |
44 | var angle = forward
45 | ? player.angle
46 | : new Raycaster.Classes.Angle(player.angle.degrees + 180);
47 |
48 | var intersection = findIntersection(angle);
49 |
50 | if (!intersection || intersection.distance > 50) {
51 | player.x = Math.round(player.x + delta.x);
52 | player.y = Math.round(player.y - delta.y);
53 | }
54 |
55 | Raycaster.engine.redraw();
56 | };
57 |
58 | // Elevate player up or down
59 | var elevate = function(up)
60 | {
61 | step = up ? Raycaster.Constants.movementStep : -Raycaster.Constants.movementStep;
62 | player.z += step;
63 |
64 | // Prevent player from going under ground
65 | if (player.z < 0) {
66 | player.z = 0;
67 | }
68 |
69 | Raycaster.engine.redraw();
70 | };
71 |
72 | // Make player strafe left or right
73 | var strafe = function(left)
74 | {
75 | var angle = left
76 | ? new Raycaster.Classes.Angle(player.angle.degrees + 90)
77 | : new Raycaster.Classes.Angle(player.angle.degrees - 90);
78 |
79 | var delta = Raycaster.Raycasting.getDeltaXY(angle, Raycaster.Constants.movementStep);
80 | intersection = findIntersection(angle);
81 |
82 | if (!intersection || intersection.distance > 20) {
83 | player.x = Math.round(player.x + delta.x);
84 | player.y = Math.round(player.y - delta.y);
85 | }
86 |
87 | Raycaster.engine.redraw();
88 | };
89 |
90 | /****************** / Public methods / *****************/
91 | // Update movement
92 | var update = function()
93 | {
94 | // Turn left
95 | if (Raycaster.Objects.keys.arrowLeft.pressed || Raycaster.Objects.keys.charQ.pressed) {
96 | turn(Raycaster.Constants.turningStep);
97 | }
98 | // Turn right
99 | else if (Raycaster.Objects.keys.arrowRight.pressed || Raycaster.Objects.keys.charE.pressed) {
100 | turn(-Raycaster.Constants.turningStep);
101 | }
102 |
103 | // Walk forward
104 | if (Raycaster.Objects.keys.arrowUp.pressed || Raycaster.Objects.keys.charW.pressed) {
105 | walk(true);
106 | }
107 | // Walk backward
108 | else if (Raycaster.Objects.keys.arrowDown.pressed || Raycaster.Objects.keys.charS.pressed) {
109 | walk(false);
110 | }
111 |
112 | // Strafe left
113 | if (Raycaster.Objects.keys.charA.pressed) {
114 | strafe(true);
115 | }
116 | // Strafe right
117 | else if (Raycaster.Objects.keys.charD.pressed) {
118 | strafe(false);
119 | }
120 |
121 | // Stop gameloop interval
122 | if (Raycaster.Objects.keys.esc.pressed) {
123 | clearInterval(Raycaster.Objects.gameloopInterval);
124 | }
125 |
126 | // Reverse player angle
127 | if (Raycaster.Objects.keys.charR.pressed) {
128 | Raycaster.Objects.player.angle.setValue(Raycaster.Objects.player.angle.degrees - 180);
129 | Raycaster.Objects.keys.charR.pressed = false;
130 | Raycaster.engine.redraw();
131 | }
132 |
133 | if (Raycaster.Objects.keys.charZ.pressed) {
134 | elevate(true);
135 | }
136 |
137 | if (Raycaster.Objects.keys.charX.pressed) {
138 | elevate(false);
139 | }
140 | }
141 |
142 | // Bind the arrow keydown events
143 | var init = function()
144 | {
145 | // Prevents default event handling
146 | var preventDefault = function(e)
147 | {
148 | e.preventDefault
149 | ? e.preventDefault()
150 | : e.returnValue = false;
151 | };
152 |
153 | // Handler for keydown events
154 | var keyDownHandler = function (e)
155 | {
156 | var keyCode = e.keyCode || e.which;
157 |
158 | for (var name in Raycaster.Objects.keys) {
159 | if (Raycaster.Objects.keys[name].code == keyCode) {
160 | Raycaster.Objects.keys[name].pressed = true;
161 | preventDefault(e);
162 | }
163 | }
164 | };
165 |
166 | // Handler for keyup events
167 | var keyUpHandler = function (e)
168 | {
169 | var keyCode = e.keyCode || e.which;
170 |
171 | for (var name in Raycaster.Objects.keys) {
172 | if (Raycaster.Objects.keys[name].code == keyCode) {
173 | Raycaster.Objects.keys[name].pressed = false;
174 | preventDefault(e);
175 | }
176 | }
177 | };
178 |
179 | window.addEventListener("keydown", keyDownHandler, false);
180 | window.addEventListener("keyup", keyUpHandler, false);
181 |
182 | // Bind key icons to mobile devices which have no keyboard
183 | var keys = document.getElementsByClassName("keys");
184 |
185 | for (var i = 0, n = keys.length; i < n; ++i) {
186 | var key = keys[i];
187 | var keyCode = parseInt(key.getAttribute("data-code"), 0);
188 |
189 | (function(k, kc) {
190 | k.addEventListener("mouseover", function() {
191 | keyDownHandler({ keyCode: kc });
192 | return false;
193 | }, false);
194 | k.addEventListener("mouseout", function() {
195 | keyUpHandler({ keyCode: kc });
196 | return false;
197 | }, false);
198 | }(key, keyCode));
199 | }
200 |
201 | // Redraw when settings change
202 | document.getElementById("chkTextures").addEventListener('change', Raycaster.engine.redraw);
203 | document.getElementById("chkLighting").addEventListener('change', Raycaster.engine.redraw);
204 | document.getElementById("chkMiniMap").addEventListener('change', Raycaster.engine.redraw);
205 | document.getElementById("chkSky").addEventListener('change', Raycaster.engine.redraw);
206 | //document.getElementById("chkFloor").addEventListener('change', Raycaster.engine.redraw);
207 | document.getElementById("chkSprites").addEventListener('change', Raycaster.engine.redraw);
208 |
209 | // Level and size selection
210 | document.getElementById("ddlLevel").addEventListener('change', function() {
211 | location.hash = document.getElementById("ddlLevel").selectedIndex + "," + Raycaster.Constants.screenWidth;
212 | location.reload();
213 | });
214 | document.getElementById("ddlSize").addEventListener('change', function() {
215 | location.hash = document.getElementById("ddlLevel").selectedIndex + "," +
216 | document.getElementById("ddlSize").options[document.getElementById("ddlSize").selectedIndex].value;
217 | location.reload();
218 | });
219 | };
220 |
221 | return {
222 | update: update,
223 | init: init
224 | };
225 | }();
--------------------------------------------------------------------------------
/js/raycaster.renderengine.js:
--------------------------------------------------------------------------------
1 | /*
2 | // Namespace: Raycaster.RenderEngine
3 | // Description: The render engine class contains all methods required for drawing the 3D world and related stuff.
4 | // Reasons for making this non-static is that the instance needs to be created after the canvas context is created.
5 | // Raycaster.start() creates the RenderEngine instances which is accessible through Raycaster.engine
6 | //
7 | // Public methods: redraw()
8 | // update()
9 | */
10 | Raycaster.RenderEngine = function()
11 | {
12 | var context = Raycaster.Objects.context,
13 | constants = Raycaster.Constants,
14 | objects = Raycaster.Objects,
15 | drawing = Raycaster.Drawing,
16 | classes = Raycaster.Classes,
17 | raycasting = Raycaster.Raycasting,
18 | lastFpsUpdate = new Date().getTime();
19 |
20 | // Calculate viewport distance and angle between casted rays
21 | constants.distanceToViewport = Math.round(constants.screenWidth / 2 / Math.tan(constants.fieldOfView / 2 * (Math.PI / 180)));
22 | constants.angleBetweenRays = Number(constants.fieldOfView / constants.screenWidth);
23 |
24 |
25 | /************* / Rendering of secondary stuff (sky, floor, map) / *****************/
26 |
27 | // Writes debug information to the screen
28 | var drawDebugInfo = function()
29 | {
30 | if (constants.displayDebugInfo) {
31 | var elapsed = new Date().getTime() - lastFpsUpdate,
32 | fps = Math.round(1000 / elapsed),
33 | fpsText = fps + " fps";
34 |
35 | Raycaster.Drawing.text(fpsText, 590, 10, Raycaster.Drawing.colorRgb(255, 255, 255), Raycaster.Constants.debugFont);
36 | lastFpsUpdate = new Date().getTime();
37 |
38 | Raycaster.Drawing.text("X: " + Math.round(objects.player.x) + " Y: " + Math.round(objects.player.y) + " Z: " + Math.round(objects.player.z), 530, 30, Raycaster.Drawing.colorRgb(255, 255, 255), Raycaster.Constants.debugFont);
39 | Raycaster.Drawing.text("A: " + objects.player.angle.degrees, 590, 50, Raycaster.Drawing.colorRgb(255, 255, 255), Raycaster.Constants.debugFont);
40 | }
41 | }
42 |
43 | // Draw the 2D top-view of the level
44 | var drawMiniMap = function()
45 | {
46 | if (objects.settings.renderMiniMap()) {
47 | // Map is smaller than world, determine shrink factor
48 | var shrinkFactor = 5,
49 | mapOffsetX = 0,
50 | mapOffsetY = 0,
51 | odd = false;
52 |
53 | context.globalAlpha = 0.6;
54 |
55 | // Draw white background
56 | drawing.square(mapOffsetX, mapOffsetY,
57 | 160, 160,
58 | drawing.colorRgb(255, 255, 255));
59 |
60 | // Draw elevations
61 | for (var i in Raycaster.Objects.Level.elevations) {
62 | var elevation = Raycaster.Objects.Level.elevations[i],
63 | area = elevation.area;
64 |
65 | drawing.square(area.x1 / shrinkFactor, area.y1 / shrinkFactor, (area.x2 - area.x1) / shrinkFactor, (area.y2 - area.y1) / shrinkFactor, drawing.colorRgb(255, 0, 0));
66 | }
67 |
68 | // Draw the walls
69 | for (var i in Raycaster.Objects.Level.walls) {
70 | var wall = Raycaster.Objects.Level.walls[i];
71 | drawing.line(mapOffsetX + wall.x1 / shrinkFactor, mapOffsetY + wall.y1 / shrinkFactor,
72 | mapOffsetX + wall.x2 / shrinkFactor, mapOffsetY + wall.y2 / shrinkFactor, drawing.colorRgb(0, 0, 0));
73 | }
74 |
75 | // Draw sprites
76 | for (var i in Raycaster.Objects.Level.sprites) {
77 | var sprite = Raycaster.Objects.Level.sprites[i];
78 | drawing.circle(mapOffsetX + sprite.x / shrinkFactor, mapOffsetY + sprite.y / shrinkFactor, 2, drawing.colorRgb(0, 255, 0));
79 | }
80 |
81 | // Draw player
82 | var playerX = Math.floor(objects.player.x / shrinkFactor),
83 | playerY = Math.floor(objects.player.y / shrinkFactor);
84 |
85 | drawing.circle(mapOffsetX + playerX, mapOffsetY + playerY, 3, drawing.colorRgb(255, 0, 0));
86 |
87 | // Visualize the viewing range on the map
88 | var angle = new classes.Angle(objects.player.angle.degrees + constants.fieldOfView / 2),
89 | rayStep = 10;
90 |
91 | for (var i = 0; i < constants.screenWidth; i += rayStep)
92 | {
93 | var deltaX = Math.floor(Math.cos(angle.radians) * (Math.abs(200) / shrinkFactor)),
94 | deltaY = Math.floor(Math.sin(angle.radians) * (Math.abs(200) / shrinkFactor));
95 |
96 | drawing.line(mapOffsetX + playerX, mapOffsetY + playerY,
97 | playerX + deltaX, playerY - deltaY, drawing.colorRgb(200, 200, 0));
98 |
99 | angle.turn(-constants.angleBetweenRays * rayStep);
100 | }
101 |
102 | context.globalAlpha = 1;
103 | }
104 | };
105 |
106 | // Draw sky background
107 | var drawSky = function()
108 | {
109 | if (objects.settings.renderSky()) {
110 | var skyX = objects.skyImage.width - parseInt(objects.player.angle.degrees * (objects.skyImage.width / 360)),
111 | skyWidth = constants.screenWidth,
112 | leftOverWidth = 0;
113 |
114 | if (skyX + skyWidth > objects.skyImage.width) {
115 | leftOverWidth = skyX + skyWidth - objects.skyImage.width;
116 | skyWidth -= leftOverWidth;
117 | }
118 |
119 | if (skyWidth > 0) {
120 | context.drawImage(objects.skyImage,
121 | skyX, 0, skyWidth, constants.screenHeight / 2,
122 | 0, 0, skyWidth, constants.screenHeight / 2);
123 | }
124 |
125 | if (leftOverWidth > 0) {
126 | context.drawImage(objects.skyImage,
127 | 0, 0, leftOverWidth, constants.screenHeight / 2,
128 | skyWidth, 0, leftOverWidth, constants.screenHeight / 2);
129 | }
130 | }
131 | }
132 |
133 | // Draws the gradient for the floor
134 | var drawFloorGradient = function()
135 | {
136 | var context = Raycaster.Objects.context,
137 | gradient = context.createLinearGradient(0, constants.screenHeight / 2, 0, constants.screenHeight);
138 |
139 | gradient.addColorStop(0, drawing.colorRgb(20, 20, 20));
140 | gradient.addColorStop(0.25, drawing.colorRgb(40, 40, 40));
141 | gradient.addColorStop(0.6, drawing.colorRgb(100, 100, 100));
142 | gradient.addColorStop(1, drawing.colorRgb(130, 130, 130));
143 |
144 | context.fillStyle = gradient;
145 | context.fillRect(0, constants.screenHeight / 2, constants.screenWidth, constants.screenHeight / 2);
146 | }
147 |
148 | // Perform floor casting for scanline
149 | // Floor casting is too slow at this point..
150 | // The calculations alone lose us a lot of FPS, not even to speak about drawing the pixels...
151 | // (thats why there is drawFloorGradient() to use as alternative)
152 | var drawFloor = function(vscan, startY, intersection)
153 | {
154 | var step = 1;
155 |
156 | // Formula from: http://lodev.org/cgtutor/raycasting2.html
157 | if (objects.settings.renderFloor() && vscan % step == 0) {
158 |
159 | var floorTexture = objects.textures[Raycaster.Objects.Level.floorTextureId];
160 |
161 | for (var y = startY; y < constants.screenHeight; y += step) {
162 | var curdist = constants.screenHeight / (2 * y - constants.screenHeight);
163 |
164 | var weight = curdist / intersection.distance,
165 | floorX = weight * intersection.x + (1 - weight) * objects.player.x,
166 | floorY = weight * intersection.y + (1 - weight) * objects.player.y,
167 | textureX = parseInt(floorX * floorTexture.width) % floorTexture.width,
168 | textureY = parseInt(floorY * floorTexture.height) % floorTexture.height;
169 |
170 | context.drawImage(floorTexture,
171 | textureX, textureY, 1, 1,
172 | vscan, y, step, step);
173 |
174 | //if (objects.settings.renderLighting() && curdist > 100) {
175 | // drawing.lineSquare(vscan, y, vscan + 1, y + 1, drawing.colorRgba(0, 0, 0, calcDistanceOpacity(curdist)))
176 | //}
177 | }
178 | }
179 | };
180 |
181 | /****************** / Core rendering methods (walls, sprites) / *****************/
182 |
183 | // Search for objects in given direction and draw the vertical scanline for them.
184 | // All objects in visible range will be drawn in order of distance.
185 | var drawObjects = function(vscan, angle)
186 | {
187 | var intersections = raycasting.findObjects(angle, vscan);
188 |
189 | // Draw walls for each found intersection
190 | for (var i = 0; i < intersections.length; i++) {
191 | var intersection = intersections[i];
192 | if (intersection.isSprite) {
193 | drawSprite(vscan, intersection);
194 | }
195 | else {
196 | drawWall(vscan, intersection);
197 | }
198 | }
199 |
200 | // Floor casting (still way too slow)
201 | //drawFloor(vscan, drawParams.dy2, intersection);
202 | }
203 |
204 | // Draw the vertical slice for a wall
205 | var drawWall = function(vscan, intersection)
206 | {
207 | var drawParams = intersection.drawParams;
208 |
209 | if (objects.settings.renderTextures()) {
210 | // Draw wall slice with texture
211 | context.drawImage(drawParams.texture,
212 | intersection.textureX, drawParams.sy1, 1, drawParams.sy2 - drawParams.sy1,
213 | vscan, drawParams.dy1, 1, drawParams.dy2 - drawParams.dy1);
214 | }
215 | else {
216 | // Draw without textures
217 | drawing.lineSquare(vscan, drawParams.dy1, 1, drawParams.dy2, drawing.colorRgb(128, 0, 0));
218 | }
219 |
220 | // Make walls in the distance appear darker
221 | if (objects.settings.renderLighting() && intersection.distance > constants.startFadingAt) {
222 | drawing.lineSquare(vscan, drawParams.dy1, 1, drawParams.dy2, drawing.colorRgba(0, 0, 0, calcDistanceOpacity(intersection.distance)))
223 | }
224 | }
225 |
226 | // Draw the vertical slice for a sprite
227 | var drawSprite = function(vscan, intersection)
228 | {
229 | if (objects.settings.renderSprites()) {
230 | var drawParams = intersection.drawParams;
231 |
232 | context.drawImage(drawParams.texture,
233 | intersection.textureX, drawParams.sy1, 1, drawParams.sy2 - drawParams.sy1,
234 | vscan, drawParams.dy1, 1, drawParams.dy2 - drawParams.dy1);
235 |
236 | // Make sprites in the distance appear darker
237 | if (objects.settings.renderLighting() && intersection.distance > constants.startFadingAt) {
238 | var opacity = calcDistanceOpacity(intersection.distance);
239 |
240 | // There is a black image mask of every sprite located in the sprites array, one index after the original sprite.
241 | // Draw the mask over the sprite using the calculated opacity
242 | if (opacity > 0) {
243 | context.globalAlpha = opacity;
244 | context.drawImage(objects.sprites[intersection.resourceIndex + 1],
245 | intersection.textureX, drawParams.sy1, 1, drawParams.sy2 - drawParams.sy1,
246 | vscan, drawParams.dy1, 1, drawParams.dy2 - drawParams.dy1);
247 | context.globalAlpha = 1;
248 | }
249 | }
250 | }
251 | }
252 |
253 | // Calculates the opacity for the black overlay image that is used to make objects in the distance appear darker
254 | var calcDistanceOpacity = function(distance)
255 | {
256 | var colorDivider = Number(distance / (constants.startFadingAt * 1.5));
257 | colorDivider = (colorDivider > 5) ? 5 : colorDivider;
258 |
259 | return Number(1 - 1 / colorDivider);
260 | };
261 |
262 | // Draw 3D representation of the world
263 | var drawWorld = function()
264 | {
265 | // Draw solid floor and ceiling
266 | drawing.clear();
267 | drawing.square(0, 0, constants.screenWidth, constants.screenHeight / 2, drawing.colorRgb(60, 60, 60));
268 | drawing.square(0, constants.screenHeight / 2, constants.screenWidth, constants.screenHeight / 2, drawing.colorRgb(120, 120, 120));
269 |
270 | // Draw sky and floor (floorcasting is disabled)
271 | drawSky();
272 | drawFloorGradient();
273 |
274 | var angle = new classes.Angle(objects.player.angle.degrees + constants.fieldOfView / 2);
275 |
276 | // Render the walls
277 | for (var vscan = 0; vscan < constants.screenWidth; vscan++) {
278 | drawObjects(vscan, angle);
279 | angle.turn(-constants.angleBetweenRays);
280 | }
281 | };
282 |
283 | // Calculates if the player is standing inside an elevated area and returns the elevation
284 | // If elevation has changed we need to update the player's Z coord
285 | var updateElevation = function()
286 | {
287 | for (var i in Raycaster.Objects.Level.elevations) {
288 | var elevation = Raycaster.Objects.Level.elevations[i],
289 | area = elevation.area;
290 |
291 | if (objects.player.x >= area.x1 && objects.player.x <= area.x2 &&
292 | objects.player.y >= area.y1 && objects.player.y <= area.y2)
293 | {
294 | objects.player.z = elevation.height;
295 | return;
296 | }
297 | }
298 |
299 | objects.player.z = 0;
300 | };
301 |
302 | /****************** / Public methods / *****************/
303 | // Tell renderengine that a redraw is required
304 | var redraw = function()
305 | {
306 | objects.redrawScreen = true;
307 | }
308 |
309 | // Execute all rendering tasks
310 | var update = function()
311 | {
312 | if (objects.redrawScreen) {
313 | updateElevation();
314 | drawWorld();
315 | drawMiniMap();
316 | drawDebugInfo();
317 |
318 | objects.redrawScreen = false;
319 | }
320 | }
321 |
322 | // Expose public members
323 | return {
324 | redraw: redraw,
325 | update: update
326 | };
327 | };
--------------------------------------------------------------------------------
/js/raycaster.raycasting.js:
--------------------------------------------------------------------------------
1 | /*
2 | // Namespace: Raycaster.Raycasting
3 | // Description: Functionality required for raycasting
4 | //
5 | // Public methods: findWall(angle): returns Intersection object or false
6 | // findSprite(angle): returns Intersection object or false
7 | // getWallHeight: returns height for wall at specified intersection
8 | // getDeltaXY: returns difference in x and y for a distance at specified angle
9 | */
10 | Raycaster.Raycasting = function()
11 | {
12 | var objects = Raycaster.Objects,
13 | player = objects.player,
14 | classes = Raycaster.Classes,
15 | constants = Raycaster.Constants,
16 | fishbowlFixValue = 0;
17 |
18 | /****************** / Private methods / *****************/
19 | // Determine wether an intersection is located in the quadrant we are looking at
20 | var correctQuadrant = function(intersection, angle)
21 | {
22 | var deltaX = player.x - intersection.x,
23 | deltaY = player.y - intersection.y,
24 | quadrant = 0;
25 |
26 | var roundedAngle = ~~ (0.5 + angle.degrees);
27 | if (roundedAngle == 0 || roundedAngle == 360) {
28 | return deltaX < 0;
29 | }
30 | else if (roundedAngle == 90) {
31 | return deltaY > 0;
32 | }
33 | else if (roundedAngle == 180) {
34 | return deltaX > 0;
35 | }
36 | else if (roundedAngle == 270) {
37 | return deltaY < 0;
38 | }
39 |
40 | if (deltaX < 0 && deltaY >= 0) quadrant = 1;
41 | if (deltaX >= 0 && deltaY > 0) quadrant = 2;
42 | if (deltaX > 0 && deltaY <= 0) quadrant = 3;
43 | if (deltaX <= 0 && deltaY < 0) quadrant = 4;
44 |
45 | return quadrant == angle.getQuadrant();
46 | }
47 |
48 | // Calculates the length of the hypotenuse side (angled side) of a triangle
49 | // adjacentLength: length of the side adjacent to the angle
50 | // oppositeLength: length of the side opposite of the angle
51 | var getHypotenuseLength = function(adjacentLength, oppositeLength)
52 | {
53 | return Math.sqrt(Math.pow(Math.abs(adjacentLength), 2) + Math.pow(Math.abs(oppositeLength), 2));
54 | }
55 |
56 | // Calculate value needed to manipulate distance to counter "fishbowl effect"
57 | var setFishbowlFixValue = function(vscan)
58 | {
59 | var distortRemove = new classes.Angle(constants.fieldOfView / 2);
60 | distortRemove.turn(-constants.angleBetweenRays * vscan);
61 |
62 | fishbowlFixValue = Math.cos(distortRemove.radians);
63 | };
64 |
65 | // Calculate intersection point on a line (wall)
66 | // Formula found here: http://paulbourke.net/geometry/lineline2d/
67 | var getIntersection = function(line, angle, dontRoundCoords)
68 | {
69 | // Ray line
70 | var px1 = player.x,
71 | py1 = player.y,
72 | px2 = px1 + Math.cos(angle.radians),
73 | py2 = py1 - Math.sin(angle.radians);
74 |
75 | // Some number we need to solve our equation
76 | var f1 = ((line.x2 - line.x1) * (py1 - line.y1) - (line.y2 - line.y1) * (px1 - line.x1)) /
77 | ((line.y2 - line.y1) * (px2 - px1) - (line.x2 - line.x1) * (py2 - py1));
78 |
79 | // Calculate where the ray intersects with the line
80 | var i = classes.Intersection();
81 | i.x = px1 + f1 * (px2 - px1),
82 | i.y = py1 + f1 * (py2 - py1);
83 |
84 | // Check if intersection is located on the line
85 | var hit = true,
86 | intersX = i.x,
87 | intersY = i.y,
88 | linex1 = line.x1,
89 | linex2 = line.x2,
90 | liney1 = line.y1,
91 | liney2 = line.y2;
92 |
93 | // When looking for walls we want to round the intersection coordinates before comparing them
94 | // When looking for sprites we dont want those coords rounded, otherwise the result is not exact enough
95 | if (!dontRoundCoords) {
96 | // Round the numbers using bitwise rounding hack
97 | intersX = ~~ (0.5 + i.x);
98 | intersY = ~~ (0.5 + i.y);
99 | }
100 |
101 | hit = (linex1 >= linex2)
102 | ? intersX <= linex1 && intersX >= linex2
103 | : intersX >= linex1 && intersX <= linex2;
104 | if (hit) {
105 | hit = (liney1 >= liney2)
106 | ? intersY <= liney1 && intersY >= liney2
107 | : intersY >= liney1 && intersY <= liney2;
108 | }
109 |
110 | // The formula will also return the intersections that are behind the player
111 | // Only return it if it's located in the correct quadrant
112 | if (!correctQuadrant(i, angle) || !hit) {
113 | return false;
114 | }
115 |
116 | // Calculate distance to the intersection
117 | var deltaX = player.x - i.x,
118 | deltaY = player.y - i.y;
119 |
120 | if (Math.abs(deltaX) > Math.abs(deltaY)) {
121 | i.distance = Math.abs(deltaX / Math.cos(angle.radians));
122 | }
123 | else {
124 | i.distance = Math.abs(deltaY / Math.sin(angle.radians));
125 | }
126 |
127 | if (!dontRoundCoords) {
128 | //i.x = intersX;
129 | //i.y = intersY;
130 | }
131 |
132 | return i;
133 | };
134 |
135 | // Texture mapping routine for walls
136 | // Determines which scanline of the texture to draw for this intersection
137 | var setTextureParams = function(intersection)
138 | {
139 | if (objects.settings.renderTextures()) {
140 | var wall = Raycaster.Objects.Level.walls[intersection.levelObjectId],
141 | length = getHypotenuseLength(wall.x1 - wall.x2, wall.y1 - wall.y2),
142 | lengthToIntersection = getHypotenuseLength(wall.x1 - intersection.x, wall.y1 - intersection.y);
143 |
144 | intersection.resourceIndex = Raycaster.Objects.Level.walls[intersection.levelObjectId].textureId;
145 |
146 | var textureWidth = objects.textures[intersection.resourceIndex].width,
147 | textureHeight = objects.textures[intersection.resourceIndex].height;
148 |
149 | // Wall textures are stretched in height and repeated over the width
150 | if (wall.maxHeight != textureHeight) {
151 | lengthToIntersection *= textureHeight / wall.maxHeight;
152 | }
153 |
154 | intersection.textureX = parseInt(lengthToIntersection % objects.textures[intersection.resourceIndex].width);
155 | }
156 | };
157 |
158 | /*
159 | // Method: Raycaster.Raycasting.setVSliceDrawParams
160 | // Description:
161 | // Once we know the distance to a wall or sprite, this function calculates the parameters
162 | // that are required to draw the vertical slice for it.
163 | // It also accounts for leaving away pixels of the object if it exceeds the size of the viewport.
164 | // Returns FALSE if the intersection is not visible to the player
165 | //
166 | // Input parameters:
167 | // - intersection: intersection with the object to draw
168 | //
169 | // The following parameters are calculated:
170 | // - dy1: Starting point of the slice on the destination (the screen)
171 | // - dy2: End point of the slice on the destination
172 | // - sy1: Starting point of the slice on the source (the texture image)
173 | // - sy2: End point of the slice on the source
174 | // - texture: Image object containing the texture to draw
175 | */
176 | var setVSliceDrawParams = function(intersection)
177 | {
178 | var scanlineOffsY = 0, // Additional Y-offset for the scanline (used in sprites)
179 | distance = intersection.distance * fishbowlFixValue, // Distance to the intersection
180 | rindex = intersection.resourceIndex,
181 | lindex = intersection.levelObjectId,
182 | levelObject = intersection.isSprite // Level object definition (wall or sprite)
183 | ? Raycaster.Objects.Level.sprites[lindex]
184 | : Raycaster.Objects.Level.walls[lindex],
185 | texture = intersection.isSprite // Image object containing the texture to draw
186 | ? objects.sprites[rindex]
187 | : objects.textures[rindex],
188 | objHeight = intersection.isSprite // Original height of the object at current intersection
189 | ? texture.height
190 | : getWallHeight(intersection),
191 | objMaxHeight = intersection.isSprite // Maximum possible height of the object (used in walls with angled height)
192 | ? objHeight
193 | : levelObject.maxHeight,
194 | objectZ = intersection.isSprite // Z-position of the object at current intersection
195 | ? levelObject.z
196 | : getWallZ(intersection),
197 | height = Math.floor(objHeight / distance * constants.distanceToViewport); // Height of the object on screen
198 |
199 | // horizonOffset is used for aligning walls and objects correctly on the horizon.
200 | // Without this value, everything would always be vertically centered.
201 | var eyeHeight = player.height * 0.75,
202 | base = (eyeHeight + player.z - objectZ) * 2,
203 | horizonOffset = (height - Math.floor(base / distance * constants.distanceToViewport)) / 2;
204 |
205 | // Determine where to start and end the scanline on the screen
206 | var scanlineEndY = parseInt((constants.screenHeight / 2 - horizonOffset) + height / 2),
207 | scanlineStartY = scanlineEndY - height;
208 |
209 | // Prevent the coordinates from being off-screen
210 | intersection.drawParams = classes.VSliceDrawParams();
211 | intersection.drawParams.dy1 = scanlineStartY < 0 ? 0 : scanlineStartY;
212 | intersection.drawParams.dy2 = scanlineEndY > constants.screenHeight ? constants.screenHeight : scanlineEndY;
213 | intersection.drawParams.texture = texture;
214 |
215 | if (intersection.drawParams.dy2 < 0 || intersection.drawParams.dy1 > constants.screenHeight) {
216 | return false;
217 | }
218 |
219 | // Now that we've determined the size and location of the scanline,
220 | // we calculate which part of the texture image we need to render onto the scanline
221 | // When part of the object is located outside of the screen we dont need to copy that part of the texture image.
222 | if ((!intersection.isSprite && objects.settings.renderTextures())
223 | || (intersection.isSprite && objects.settings.renderSprites()))
224 | {
225 | var scale = height / texture.height, // Height ratio of the object compared to its original size
226 | srcStartY = 0, // Start Y coord of source image data
227 | srcEndY = texture.height; // End y coord of source image data
228 |
229 | // Compensate for bottom part being offscreen
230 | if (scanlineEndY > constants.screenHeight) {
231 | var remove = (scanlineEndY - constants.screenHeight) / scale;
232 | srcEndY -= remove;
233 | }
234 |
235 | // Compensate for top part being offscreen
236 | if (scanlineStartY < 0) {
237 | var remove = Math.abs(scanlineStartY) / scale;
238 | srcStartY += remove;
239 | }
240 |
241 | // Prevent the texture from appearing skewed when wall height is angled
242 | // This is bugged:
243 | /*if (objMaxHeight > objHeight) {
244 | var maxHeight = Math.floor(objMaxHeight / distance * constants.distanceToViewport),
245 | diff = Math.abs(maxHeight - height),
246 | scale = maxHeight / texture.height;
247 |
248 | srcStartY += Math.floor(diff / scale);
249 | }*/
250 |
251 | intersection.drawParams.sy1 = srcStartY;
252 | intersection.drawParams.sy2 = srcEndY;
253 |
254 | if (intersection.drawParams.sy2 <= intersection.drawParams.sy1) {
255 | return false;
256 | }
257 | }
258 |
259 | return true;
260 | }
261 |
262 | // Walls have a start and end height
263 | // This method calculates the height of a wall at a specific intersection
264 | var getWallHeight = function(intersection)
265 | {
266 | var wall = Raycaster.Objects.Level.walls[intersection.levelObjectId];
267 |
268 | if (wall.h1 == wall.h2) {
269 | return wall.h1;
270 | }
271 |
272 | var length = getHypotenuseLength(wall.x1 - wall.x2, wall.y1 - wall.y2),
273 | slope = (wall.h2 - wall.h1) / length,
274 | lengthToIntersection = getHypotenuseLength(wall.x1 - intersection.x, wall.y1 - intersection.y),
275 | height = wall.h1 + (lengthToIntersection * slope);
276 |
277 | return height;
278 | }
279 |
280 | // Walls have a start and end Z position
281 | // This method calculates the Z pos of a wall at a specific intersection
282 | var getWallZ = function(intersection)
283 | {
284 | var wall = Raycaster.Objects.Level.walls[intersection.levelObjectId];
285 |
286 | if (wall.z1 == wall.z2) {
287 | return wall.z1;
288 | }
289 |
290 | var length = getHypotenuseLength(wall.x1 - wall.x2, wall.y1 - wall.y2),
291 | slope = (wall.z2 - wall.z1) / length,
292 | lengthToIntersection = getHypotenuseLength(wall.x1 - intersection.x, wall.y1 - intersection.y),
293 | z = wall.z1 + (lengthToIntersection * slope);
294 |
295 | return z;
296 | }
297 |
298 | // Find intersection on a specific sprite that is in the players field of view
299 | var findSprite = function(angle, spriteId)
300 | {
301 | // Create a imaginary plane on which the sprite is drawn
302 | // That way we can check for sprites in exactly the same way we check for walls
303 | var planeAngle = new classes.Angle(angle.degrees - 90),
304 | x = Raycaster.Objects.Level.sprites[spriteId].x,
305 | y = Raycaster.Objects.Level.sprites[spriteId].y,
306 | sprite = objects.sprites[Raycaster.Objects.Level.sprites[spriteId].id],
307 | delta = getDeltaXY(planeAngle, (sprite.width - 1) / 2),
308 | plane = classes.Vector(x - delta.x, y + delta.y,
309 | x + delta.x, y - delta.y);
310 |
311 | // Find intersection point on the plane
312 | var intersection = getIntersection(plane, angle, true);
313 |
314 | if (intersection) {
315 | // Determine which scanline of the sprite image to draw for this intersection
316 | var lengthToIntersection = getHypotenuseLength(plane.x1 - intersection.x, plane.y1 - intersection.y);
317 |
318 | intersection.textureX = Math.floor(lengthToIntersection);
319 | intersection.resourceIndex = Raycaster.Objects.Level.sprites[spriteId].id;
320 | intersection.levelObjectId = spriteId;
321 | intersection.isSprite = true;
322 |
323 | // Calculate the drawing parameters for the vertical scanline for this sprite
324 | // If the sprite is not visible for the player the method returns false
325 | if (!setVSliceDrawParams(intersection)) {
326 | return false;
327 | }
328 | }
329 |
330 | return intersection;
331 | };
332 |
333 | // Find intersection on a specific wall that is in the players field of view
334 | var findWall = function(angle, wallId)
335 | {
336 | // Find intersection point on current wall
337 | var intersection = getIntersection(Raycaster.Objects.Level.walls[wallId], angle);
338 |
339 | if (intersection) {
340 | intersection.levelObjectId = wallId;
341 | setTextureParams(intersection);
342 |
343 | // Calculate the drawing parameters for the vertical scanline for this wall
344 | // If the wall is not visible (elevation is too high or low) the method returns false
345 | if (!setVSliceDrawParams(intersection)) {
346 | return false;
347 | }
348 | }
349 |
350 | return intersection;
351 | };
352 |
353 |
354 | /****************** / Public methods / *****************/
355 | // Find intersection for all the walls and sprites that are in viewing range.
356 | // Returns an array of intersection objects, sorted descending by distance
357 | var findObjects = function(angle, vscan)
358 | {
359 | var intersections = new Array();
360 |
361 | if (vscan) {
362 | setFishbowlFixValue(vscan);
363 | }
364 |
365 | // Find walls
366 | for (var i = 0; i < Raycaster.Objects.Level.walls.length; i++) {
367 | var intersection = findWall(angle, i);
368 | if (intersection) {
369 | intersections[intersections.length] = intersection;
370 | }
371 | }
372 |
373 | // Find sprites
374 | for (var i = 0; i < Raycaster.Objects.Level.sprites.length; i++) {
375 | var intersection = findSprite(angle, i);
376 | if (intersection) {
377 | intersections[intersections.length] = intersection;
378 | }
379 | }
380 |
381 | // Sort the objects by distance so that the once further away are drawn first
382 | intersections.sort(function(i1, i2) {
383 | return i2.distance - i1.distance;
384 | });
385 |
386 | return intersections;
387 | };
388 |
389 | // Calculate difference in X and Y for a distance at a specific angle
390 | var getDeltaXY = function(angle, distance)
391 | {
392 | return classes.Point(
393 | Math.cos(angle.radians) * distance,
394 | Math.sin(angle.radians) * distance
395 | );
396 | }
397 |
398 | // Expose public members
399 | return {
400 | findObjects : findObjects,
401 | getDeltaXY: getDeltaXY,
402 | };
403 | }();
--------------------------------------------------------------------------------