Function Draw Mixing Properties

前端 未结 2 614
情深已故
情深已故 2021-01-23 11:40

I am trying to create a 2d platformer, and the code is the base of it.

For some unknown reason, my final function draw mixes the other functions\' properties (especiall

相关标签:
2条回答
  • 2021-01-23 11:44

    The problem is that you first draw the shape and after that you set the fill and the stroke. Doing so you set the fill and stroke for the next shape.

    In my code I'm using ctx.translate(0,-400) because otherwise the canvas would have been too large. Remove this line when setting your canvas size.

    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    //setting the canvas size
    canvas.width = 400;
    canvas.height = 200;
    
    ctx.translate(0,-400);
    
    function Shooter() {
        this.x = 100;
        this.y = 500;
        this.size = 50;
        this.color = "blue";
        this.borderColor = "black";
        this.borderWidth = 5;
        this.draw = function() {
        // first set the colors for this shape
        ctx.fillStyle = this.color;
        ctx.strokeStyle = this.borderColor;
        ctx.lineWidth = this.borderWidth;
        // then fill and stroke the shape  
        ctx.fillRect(this.x, this.y, this.size, this.size);
        ctx.strokeRect(this.x, this.y, this.size, this.size);
        }
    }
    
    function Gun() {
        this.x = sh.x + sh.size / 2 + 10;
        this.y = sh.y + sh.size / 2;
        this.color = "grey";
        this.borderColor = "brown";
        this.borderWidth = 1;
        this.width = 20;
        this.height = 10;
        this.draw = function() {
        ctx.fillStyle = this.color;
        ctx.strokeStyle = this.borderColor;
        ctx.lineWidth = this.borderWidth;
        ctx.fillRect(this.x,this.y,this.width,this.height);     ctx.strokeRect(this.x,this.y,this.width,this.height);
    
        }
    }
    
    function Bullet() {
        this.x = sh.x + sh.size * 2;
        this.y = sh.y + sh.size / 2;
        this.color = "orange";
        this.radius = 5;
        this.vx = 20;
        this.borderColor = "green";
        this.borderWidth = 2;
        this.draw = function() {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
            ctx.fillStyle = this.color;
            ctx.strokeStyle = this.borderColor;
            ctx.lineWidth = this.borderWidth;
            ctx.fill();
            ctx.stroke();
          
        }
    }
    var sh = new Shooter();
    var g = new Gun();
    var b = new Bullet();
    
    function draw() {
      
        sh.draw();
    
        g.draw();
        
        b.draw();
        
    requestAnimationFrame(draw);
    }
    
    draw();
    canvas{border:1px solid}
    <canvas id="canvas"></canvas>

    0 讨论(0)
  • 2021-01-23 12:06

    Some extra points.

    The existing answer will solve your problem, however I have some time and noticed some points about your code that can be improved.

    Performance is king

    When writing games (Or for that matter animating content) you will get to a point where the complexity of the animation (number of animated and drawn items) gets to a stage where the device can no longer do it at full frame rate. This become more of a problems as you try to cover a larger range of devices.

    To get the most speed per line of code in Javascript you need to be aware of some simple rules in regard to objects and how they are created and destroyed (free memory for other objects).

    JavaScript is managed

    This means that as a programmer you don't need to worry about memory. You create an object and the memory for it is found for you. When you no longer need the object javascript will cleanup the memory so it is free for other objects.

    This make programing in Javascript easier. However in animations this can become a problem as the code that manages the memory allocation and clean up (AKA delete allocated memory or GC Garbage Collection) takes time. If your animation is taking a long time to compute and render each frame then GC is forced to block your script and cleanup.

    This management of memory is a the biggest source of Jank in Javascript animations (games).

    It also introduces extra processing to create objects, as free memory has to be located and allocated when you create a new object. It gets worse on low end devices with low amounts of RAM. Creating new objects will more often force GC to free up memory for the new object (stealing precious CPU cycles). This make performance on low end devices not a linear degradation but rather a logarithmic degradation.

    Types of Objects.

    Objects (player, pickups, bullets, FX) can be classified by how long they live and how many can exist at one time. The lifetime of the object means you can take advantages of how JS manages memory to optimise objects and memory use.


    Single instance object.

    These are objects that exist only once in the animation. That is have a lifetime from start to end (in a game that may be from level start to level end).

    Eg the player, the score display.

    Example

    The best ways to create these objects is as a singleton or object factory. The bullets example below this uses a singleton

    EG Object factory to create player

    function Shooter() {
        // Use closure to define the properties of the object
        var x = 100;
        var y = 500;
        const size = 50;
        const style = {
            fillStyle : "blue",
            strokeStyle : "black",
            lineWidth : 5,
        }
        // the interface object defines functions and properties that
        // need to be accessed from outside this function
        const API = {
            draw() {
                ctx.fillStyle = style.fillStyle;
                ctx.strokeStyle = style.strokeStyle;
                ctx.lineWidth = style.lineWidth;
    
                ctx.fillRect(x, y, size, size);
                ctx.strokeRect(x, y, size, size);
    
                // it is quicker to do the above two lines as
                /*
                ctx.beginPath(); // this function is done automatically 
                                 // for fillRect and strokeRect. It                                 
                ctx.rect(x, y, size, size);
                ctx.fill();
                ctx.stroke();
                */
            }
        }
        return API;
    }
    

    You create an use it just like any other object

    const player = new Shooter();
    // or
    const player = Shooter();  / You dont need the new for this type of object
    
    // to draw
    
    player.draw();
    

    Many instance object.

    These are objects that have very short lives, maybe a few frames. They can also exist in the hundreds (think of sparks FX in an explosion, or rapid fire bullets)

    In your code you have only one bullet, but I can imagine that you could have many, or rather than a bullets, it could be gribble or spark FXs.

    Instantiation

    Creating objects requires CPU cycles. Modern JS has many optimisations and thus there is not much difference in how you create objects. But there is still a difference and using the best method pays off, especially if you do it 1000's of times a second. (the last JS game I wrote handles upto 80,000 FX objects a second, most live no more than 3-6 frames)

    For many short lived objects define a prototype or use the class syntax (this reduces creating time by around 50%). Store items in a pool when not used to stop GC hits and reduce instantiation overheads. Be render smart, and don't needlessly waste time waiting for GPU to do pointless state changes.

    Memory

    These short lived objects are the biggest source of slowdown and JANK due to memory management overhead creating and deleting them creates.

    To combat the memory management overhead you can do that management yourself. The best way is complicated (use a pre allocated (at level start) bubble array and sets a max number for each object during the level).

    Object Pool

    The simplest solutions that will get you 90% of the best and allows for unlimited (depends on total RAM) is to use object pools.

    A pool is an array of unused objects, that you would normally have let GC delete. Active objects are stored in an array. They do their thing and when done they are moves from the object array into the pool.

    When you need a new object rather than create it with new Bullet() you first check if the pool has any. If it does you take the old object from the pool reset its properties and put it on the active array. If the pool is empty you create a new object.

    This means that you never delete an object (over the lifetime of the level / animation). Because you check the pool each time you create the max memory the bullets will use will actually be less than creating new objects each time (GC does not delete immediately)

    Rendering

    The 2D context is a great API for rendering, but use it badly and you can kill the frame rate without changing the rendered appearance.

    If you have many objects that use the same style. Don't render them as separate path. Defined one path, add the objects then fill and stroke.

    Example

    Example of a rapid fire buller pool. All bullets have the same style. This interface hides the bullets from the main code. You can only access the bullets API

    const bullets = (() => { // a singleton
        function Bullet() { }
        const radius = 5;
        const startLife = 100;
        this.radius = 5;
        const style = {
            fillStyle : "orange",
            strokeStyle : "green",
            lineWidth : 2,
        }    
        // to hold the interface 
        Bullets.prototype = {
            init(x,y,dx,dy) { // dx,dy are delta
                this.life = startLife;
                this.x = x;
                this.y = y;
                this.dx = dx;
                this.dy = dy;
            },
            draw() {
                ctx.arc(this.x, this.y, radius, 0 , Math.PI * 2);
            },
            move() {
                this.x += this.dx;
                this.y += this.dy;
                this.life --;
            }
        };
        const pool = []; // holds unused bullets
        const bullets = []; // holds active bullets
        // The API that manages the bullets
        const API = {
            fire(x,y,dx,dy) {
                var b;
                if(pool.length) {
                    b = bullets.pop();
                } else {
                    b = new Bullet();
                }               
                b.init(x,y,dx,dy);
                bullets.push(bullets); // put on active array
            },
            update() {
                var i;
                for(i = 0; i < bullets.length; i ++) {
                    const b = bullets[i];
                    b.move();
                    if(b.life <= 0) { // is the bullet is no longer needed move to the pool
                        pool.push(bullets.splice(i--, 1)[0]);
                    }
                }            
            },
            draw() {
                ctx.lineWidth = style.lineWidth;
                ctx.fillStyle = style.fillStyle;
                ctx.strokeStyle = style.strokeStyle;
                ctx.beginPath();
                for(const b of bullets) { b.draw() }
                ctx.fill();
                ctx.stroke();            
            },
            get count() { return bullets.length }, // get the number of active
            clear() { // remove all 
                pool.push(...bullets); // move all active to the pool;
                bullets.length = 0; // empty the array;
            },
            reset() { // cleans up all memory
                pool.length = 0;
                bullets.length = 0;
            }
        };
        return API;
    })();
    

    To use

    ...in the fire function

    // simple example
    bullets.fire(gun.x, gun.y, gun.dirX, gun.dirY);
    

    ...in the main render loop

    bullets.update(); // update all bullets
    if(bullets.count) { // if there are bullets to draw
        bullets.draw();
    }        
    

    ...if restarting level

    bullets.clear(); // remove bullets from previouse play
    

    ...if at end of level free the memory

    bullets.clear();    
    

    The somewhere in between object.

    These are objects that lay somewhere in between the above two types,

    Eg a power ups, background items, enemy AI agents.

    If object does not get created in high numbers, and have lives that may last more than a second to less than a complete level you need to ensure that they can be instantiated using the optimal method. (Personally I use pools (bubble array) for all but objects that live for the life of the level, but that can result in a lot of code)

    Define the prototype

    There are two way to effectively create these objects. Using the class syntax (personally hate this addition to JS) or define the prototype outside the instantiating function.

    Example

    function Gun(player, bullets) {
        this.owner = player;
        this.bullets = bullets; // the bullet pool to use.
        this.x = player.x + player.size / 2 + 10;
        this.y = player.y + player.size / 2;
    
        this.width = 20;
        this.height = 10;
        const style = {
            fillStyle : "grey",
            strokeStyle : "brown",
            lineWidth : 1,
        };   
    }
    
    // Moving the API to the prototype improves memory use and makes creation a little quicker
    Gun.prototype = {
        update() {
            this.x = this.owner.x + this.owner.size / 2 + 10;
            this.y = this.owner.y + this.owner.size / 2;
        },
        draw() {
            ctx.lineWidth = this.style.lineWidth;
            ctx.fillStyle = this.style.fillStyle;
            ctx.strokeStyle = this.style.strokeStyle;
            ctx.beginPath();
            ctx.rect(this.x,this.y,this.width,this.height);
            ctx.fill();
            ctx.stroke();
        },
        shoot() {
            this.bullets.fire(this.x, this.y, 10, 0);
        },
    }    
    

    Hope this helps. :)

    0 讨论(0)
提交回复
热议问题