Doom Style Simple 3D in Javascript

Tue, 09/12/2017 - 17:04

A LOOOooong time ago...well 2001-2002'ish (I think). I was playing around a lot with Game Maker by Mark Overmars. There was developer named Brent who went by the handle Freegadgets who made a 3D engine Gadget 3D. It was really quite impressive. He started off by simply remaking a simple version of Doom. Over time he expanded it and eventually it used actual 3D polygons. It was loads of fun to build games with it and very easy. It was also a great learning tool. (As with MANY of my old projects I lost most of the games and models I built during this period). I always wanted to build something using the same concept and so, what better time than now. 

So this is my first version using the same concept.

pseudo 3d explainedThe originals Doom and Castle Wolfenstein didn't use 3D (in the sense of what we think of it today). The game was primarily 2D an the game characters were sprites that were resized based on the position of the camera using some trigonometry. There were some Y access game elements. But not the player didn't generally go up or down very much. The walls appeared to be texture mapped but these were actually pre-rendered animated sprites that were displayed accordingly to where the camera was located. If I recall correctly texture UV mapping which, I believe was an MIT project, wasn't available until the mid-1990's...I could be wrong.

I have built some 3D experiments using real 3D and Javascript in the past (not including using paper.js), but actually using vanilla Javascript. I built an experiment where I could import a 3D model from the 3D tool Anim8or.

I may re-build and even expand upon that here. But this time I will import Blender models instead.

So anyways this demo is what I have so far.

http://wesmantooth.jeremyheminger.com/wip/pseudo3D/

You will notice on the right a visible representation of what's going on. Think of it as though you are the character standing on the surface of the computer monitor.

Controls:

Up, Down, Left, Right keys and A / D to strafe.

Here is the source code:

https://github.com/061375/Wes.Mantooth/blob/master/wip/pseudo3D/index.php

<script>
        const   W = (window.innerHeight-20),
                H = (window.innerHeight-20),
                hW = W/2,
                hH = H/2,
                TSPEED = 3,
                WSPEED = 5;
        window.onload = function() {

            'use strict';
            
            $w.add_object(
                1,
                Camera,{},
                document.getElementById('target2'),
                W,H
            );
            $w.add_object(
                1,
                NPCflatView,{},
                document.getElementById('target2'),
                W,H
            );
            $w.add_object(
                10,
                NPC,{},
                document.getElementById('target'),
                W,H
            );
            $w.loop(true);
        }
        /**
         * Camera
         * 
         * @param {Object}
         * @returns {Void}
         * */
        var Camera = function(o){
                this.i = o.i;
                this.d = 0;
                this.x = hW;
                this.y = hH;
                this.fov = 850;
                this.count = 0;
                this.fps = 5;
                this.view = {x:null,y:null,r:400};
                $w.canvas.zIndex(this.i,9999);
                $w.game.bindkeys({
                    ArrowLeft:Camera.prototype.Aleft
                },"keydown",document,this);
                $w.game.bindkeys({
                    ArrowRight:Camera.prototype.Aright
                },"keydown",document,this);
                $w.game.bindkeys({
                    ArrowUp:Camera.prototype.Aup
                },"keydown",document,this);
                $w.game.bindkeys({
                    ArrowDown:Camera.prototype.Adown
                },"keydown",document,this);
                $w.game.bindkeys({
                    KeyA:Camera.prototype.Aa
                },"keydown",document,this);
                $w.game.bindkeys({
                    KeyD:Camera.prototype.Ad
                },"keydown",document,this);
            }
            /**
             * loop
             * @returns {Void}
             * */
            Camera.prototype.loop = function() {
                $w.canvas.clear(this.i);
                this.drawCamera(this.i,this.x,this.y,this.d);
                if (this.d > 360) this.d = 0;
                if (this.d < 0) this.d = 360;
            }
            /**
             * drawCamera
             * @param {Number} 
             * @param {Number}
             * @param {Number}
             * @param {Number}
             * @returns {Void}
             * */
            Camera.prototype.drawCamera = function(i,x,y,d) {
                const dd = -d;
                $w.canvas.polygon(i,[
                    this.calcPoint(x,y,(dd+45),5),
                    this.calcPoint(x,y,(dd+135),5),
                    this.calcPoint(x,y,(dd+225),5),
                    this.calcPoint(x,y,(dd+315),5)
                ],'#000000','fill');
                $w.canvas.polygon(i,[
                    this.calcPoint(x,y,(dd+90),5),
                    this.calcPoint(x,y,(dd+100),10),
                    this.calcPoint(x,y,(dd+80),10)
                ],'#000000','fill');
                const c = this.calcPoint(x,y,(dd+90),this.view.r);
                $w.canvas.circle(i,c[0],c[1],this.view.r,'#FF0000',0.5);
                this.view.x = c[0];
                this.view.y = c[1];
            }
            /**
             * calcPoint
             * @param {Number} 
             * @param {Number}
             * @param {Number}
             * @returns {Array} [x,y]
             * */
            Camera.prototype.calcPoint = function(x,y,d,r){
                const a = $w.math.radians(d);
                x = x + Math.cos(a)*r;
                y = y + Math.sin(a)*r;
                return [x,y];
            }
            /**
             * Aleft
             * @param {Object} event
             * @param {Object} this
             * @returns {Void}
             * */
            Camera.prototype.Aleft = function(e,s) {
                s.d-=TSPEED;
            }
            /**
             * Aright
             * @param {Object} event
             * @param {Object} this
             * @returns {Void}
             * */
            Camera.prototype.Aright = function(e,s) {
                s.d+=TSPEED;
            }
            /**
             * Aup
             * @param {Object} event
             * @param {Object} this
             * @returns {Void}
             * */
            Camera.prototype.Aup = function(e,s) {
                s.x+=Math.sin($w.math.radians(s.d))*WSPEED;
                s.y+=Math.cos($w.math.radians(s.d))*WSPEED;
            }
            /**
             * Adown
             * @param {Object} event
             * @param {Object} this
             * @returns {Void}
             * */
            Camera.prototype.Adown = function(e,s) {
                s.x-=Math.sin($w.math.radians(s.d))*5;
                s.y-=Math.cos($w.math.radians(s.d))*5;
            }
            /**
             * Aa
             * @param {Object} event
             * @param {Object} this
             * @returns {Void}
             * */
            Camera.prototype.Aa = function(e,s) {
                s.x-=Math.sin($w.math.radians((s.d+90)))*5;
                s.y-=Math.cos($w.math.radians((s.d+90)))*5;
            }
            /**
             * Ad
             * @param {Object} event
             * @param {Object} this
             * @returns {Void}
             * */
            Camera.prototype.Ad = function(e,s) {
                s.x-=Math.sin($w.math.radians((s.d-90)))*5;
                s.y-=Math.cos($w.math.radians((s.d-90)))*5;
            }
        /**
         * NPCflatView
         * dummy object to create a canvas to hold the 2D representations of the NPCs
         * @param {Object}
         * @returns {Void}
         * */
        var NPCflatView = function(o){
            $w.canvas.zIndex(o.i,9998);
            this.loop = function(){}}
        /**
         * NPC
         * non player character
         * @param {Object}
         * @returns {Void}
         * */   
        var NPC = function(o) {
            // comment this out to set random positions
                //o.x = o.i*80;
                //o.y = 800;;
                
                if (typeof o.x === 'undefined'){
                    this.x = $w.math.frandom(W);
                }else{
                    this.x = o.x;
                }
                if (typeof o.y === 'undefined'){
                    this.y = $w.math.frandom(H);
                }else{
                    this.y = o.y;
                }
                
                this.camera = $w.objects.Camera[0],
                this.i = o.i,
                this.dx,
                this.dy,
                this.dz,
                this.fov = this.camera.fov,
                this.scale = 0,
                this.angle,
                this.color = $w.color.random(),
                this.radius,
                this.scaleRatio,
                this.size = 100;
                // draw a point to represent the 2D location of this NPC
                $w.canvas.circle(1,this.x,this.y,5);
            }
            NPC.prototype.loop = function() {
                $w.canvas.clear(this.i);
                if (!$w.collision.checkCircle(this.camera.view.x,this.camera.view.y,this.camera.view.r,this.x,this.y,5)) {
                    this.cr = $w.math.radians(this.camera.d);
                    this.dx = this.x-this.camera.x;
                    this.dz = this.y-this.camera.y;
                    this.dy = hH;
                    this.angle = Math.atan2(this.dz,this.dx);
                    this.radius = Math.sqrt(this.dx*this.dx + this.dz*this.dz) * 4;
                    this.dx = Math.cos(this.angle+this.cr) * this.radius;
                    var dis = $w.motion.distance_to_point(this.x,this.y,this.camera.x,this.camera.y);
                    $w.canvas.zIndex(this.i,-dis);
                    this.scaleRatio = this.fov/(this.fov+(dis*5));
                    this.dx = this.dx * this.scaleRatio;
                    this.scale = this.scaleRatio * this.size;
                    $w.canvas.circle(this.i,this.dx+hW,this.dy,this.scale,this.color);
                }
            }
    </script>