SOURCE

function GameEvents(){
    this._events = {};
}

GameEvents.prototype.addEvent = function(name , callback){
    this._events[name] = this._events[name] || [];
    this._events[name].push(callback);
}
GameEvents.prototype.removeEvent = function(name , callback){
    if(!callback){
        delete this._events[name];
    }else{
        this._events[name].forEach(
            (c , index) => {
                if(c === callback){
                    this._events[name].splice(index , 1);
                }
            }
        )
    }
}
GameEvents.prototype.emit = function(opts){
    let name = opts['name'] || null;
    let self = opts['self'] || null;
    let args = opts['args'] || null;
    name && this._events[name] && this._events[name].forEach(
        item => {
            item.apply(self , args);
        }
    )
}
GameEvents.prototype.hasEvent = function(name){
    if(this._events[name]){
        return true;
    }else{
        return false;
    }
}

function GameSceen(){
    this.bgColor = '#000';
    this.components = new ComponentManage(this);
    this.width = 0;
    this.height = 0;
}

GameSceen.prototype.initScene = function(g){
    this.width = g.width;
    this.height = g.height;
    this.parent = g;
    this.components.reState();
}
GameSceen.prototype.init = function(self , options){
    this.components.reState('' , options);
}
GameSceen.prototype.draw = function(){
    background(this.bgColor);
    this.components.draw(this);
}

function ComponentManage(_self , opts){
    opts = opts || {};
    this._components = {};
    this._display = opts.display || 'display';
    this._self = _self;
}

ComponentManage.prototype.hide = function(componentName){
    if(this._components[componentName]){
        this._components[componentName][this._display] = false;
    }
    return this;
}
ComponentManage.prototype.show = function(componentName){
    if(this._components[componentName]){
        this._components[componentName][this._display] = true;
    }
    return this;
}
ComponentManage.prototype.excludeShow = function(componentName){
    for(var i in this._components){
        if(i !== componentName){
            this._components[i][this._display] = false;
        }else{
            this._components[i][this._display] = true;
        }
    }
    return this;
}
ComponentManage.prototype.excludeHide = function(componentName){
    for(var i in this._components){
        if(i !== componentName){
            this._components[i][this._display] = false;
        }else{
            this._components[i][this._display] = true;
        }
    }
    return this;
}
ComponentManage.prototype.add = function(name , component , display){
    component.parent = this._self;
    display =  display === undefined ? true : !!display;
    this._components[name] = {
        name: name,
        component: component,
        initState: {
            display: display
        }
    };
    this._components[name][this._display] = display;
    return this;
}
ComponentManage.prototype.draw = function(context){
    for(var i in this._components){
        if(this._components[i][this._display]){
            this._components[i].component.draw && this._components[i].component.draw(context);
        }
    }
}
ComponentManage.prototype.reState = function(name , options){
    for(var i in this._components){
        if(i == name || !name){
            if(this._components[i].component.init && typeof this._components[i].component.init == 'function'){
                this._components[i].component.init(this._components[i].component.parent , options)
            }
            this._components[i][this._display] = this._components[i].initState.display;
        }
    }
}
ComponentManage.prototype.get = function(name){
    return this._components[name] ? this._components[name].component : null;
}


function Games(gameOptions){
    this.width = gameOptions.width;
    this.height = gameOptions.height;
    this._state = '';
    this.scenes = new ComponentManage(this);
    this.currentFps = 0;
    this.fps = gameOptions.fps || 50;
    createCanvas(this.width, this.height);
    frameRate(this.fps);
    this.times = {
        continued: 0,
        time: 0
    };
    this._timer = null;
    this.events = {
        click: false,
        dblClick: false,
        keyIsPressed: false,
        keyCode: null
    }
}

Games.prototype.start = function(){
    clearInterval(this._timer);
    this._timer = setInterval(function(){
        this.times.time += 1;
    }.bind(this) , 1000);
}

Games.prototype.setScene = function(sceneName , options){
    this._state = sceneName;
    this.scenes.reState(sceneName , options);
}

Games.prototype.addScene = function(sceneName , scene , show){
    if(scene['initScene'] && typeof scene['initScene'] === 'function'){
        scene.initScene(this);
    }
    this.scenes.add(sceneName , scene);
    if(show){
        this._state = sceneName;
    }
}

Games.prototype.draw = function(){

    this.events.keyIsPressed = keyIsPressed;
    this.events.keyCode = keyCode;

    if(this.currentFps){
        this.times.continued += round(1000 / this.currentFps , 2);
    }

    if(this._state){
        this.scenes.excludeShow(this._state);
        this.scenes.draw(this);
    }

    if(this.times.continued >= 1000){
        this.times.continued = 0;
    }
    this.events = {
        click: false,
        dblClick: false,
        keyIsPressed: false,
        keyCode: null
    }
}


function Menus(menuList , opts){
    opts = opts || {};
    this._opts = opts;
    this.menuList = menuList;
    this._events = new GameEvents();
    this.init();
}
Menus.prototype.init = function(){
    let opts = this._opts || {};
    this.stroke = opts.stroke || '#fff';
    this.textFill = opts.textFill || '#Fff';
    this.radius = opts.radius || [15 , 0 , 15 , 0];
    this.textSize = opts.textSize || 26;
    this.baseY = opts.baseY || -60;
    this.btnMinWidth = opts.btnMinWidth || 200;
    this.btnMinHeight = opts.btnMinHeight || 40;
    this.textPadding = opts.textPadding || 30;
    this.btnMargin = opts.btnMargin || 20;
    this.fill = opts.fill || [0,0,0];
}
Menus.prototype._click = function(menus , g){
    if(g.parent.events.click){
        let clickItem = null;
        menus.forEach(
            item => {
                if(mouseX <= item.x + item.w && mouseX >= item.x && mouseY <= item.y + item.h && mouseY >= item.y){
                    clickItem = item;
                }
            }
        )
        if(clickItem && clickItem.action){
            this._events.emit({
                name: 'click',
                args: [clickItem],
                self: this
            });
        }
        g.parent.events.click = false;
    }
}
Menus.prototype._setHover = function(menus){
    let hover = false;
    menus.forEach(
        item => {
            if(mouseX <= item.x + item.w && mouseX >= item.x && mouseY <= item.y + item.h && mouseY >= item.y){
                hover = true;
            }
        }
    );
    cursor(hover ? HAND : null);
}
Menus.prototype._drawBtn = function(item , x , y , w , h){
    if(mouseX <= x + w && mouseX >= x && mouseY <= y + h && mouseY >= y){
        fill(item.hover.bg);
        stroke(item.hover.stroke);
    }else{
        fill(color(this.fill[0] , this.fill[1] , this.fill[2] , map(0.75 , 0 , 1 , 0 , 255)));
        stroke(this.stroke);
    }
    let radius = item.radius || this.radius;
    rect( x, y , w , h , radius[0] , radius[1] , radius[2] , radius[3]);
}
Menus.prototype.draw = function(g){
    var baseY = g.height + this.baseY;
    var menus = this.menuList;
    for(var i = menus.length - 1; i >= 0; i--){
        let item = menus[i];

        textSize(item.textSize || this.textSize);

        let textW = textWidth(item.name);
        let btnWidth = textW + (this.textPadding * 2);
        let btnHeight = (item.height || this.btnMinHeight);
        let btnMinWidth = item.btnMinWidth || this.btnMinWidth;

        btnWidth = btnWidth < btnMinWidth ? btnMinWidth : btnWidth;

        this._drawBtn(
            item,
            g.width / 2 - (btnWidth / 2) , 
            baseY - btnHeight ,
            btnWidth ,
            btnHeight
        );

        noStroke();
        fill(item.textFill || this.textFill);
        textAlign(LEFT , CENTER);

        text(
            item.name ,
            g.width / 2 - (textW / 2) , 
            baseY - (btnHeight),
            btnWidth ,
            btnHeight
        );

        menus[i].x = g.width / 2 - (btnWidth / 2) ;
        menus[i].y = baseY - btnHeight;
        menus[i].w = btnWidth;
        menus[i].h = btnHeight;
        
        baseY -= btnHeight + this.btnMargin;
    }
    this._setHover(menus);
    this._click(menus , g);
}
Menus.prototype.onClick = function(clickCallback){
    this._events.addEvent('click' , clickCallback);
}



var games = null;

function doubleClicked(){
    games.events.dblClick = true;
}
function mouseClicked(){
    games.events.click = true;
}

function index(games){
    var indexPage = new GameSceen();
    var indexBgComponent = {
        foods: [],
        draw: function(page){
            let g = page.parent;
            if(g.times.continued >= 1000){
                this.foods.push({
                    width: floor(random(7 , 15)),
                    x: floor(random(10 , g.width - 10)),
                    y: 0,
                    speed: floor(random(1 , 5))
                })
            }
            let foods = this.foods;
            for(var i = foods.length - 1; i >= 0; i--){
                foods[i].y += foods[i].speed;
                circle(foods[i].x , foods[i].y , foods[i].width)
            }
            fill(color(0,0,0,map(0.25 , 0 , 1 , 0 , 255)));
            rect(0,0,g.width , g.height);
        },
        init: function(){
            this.foods = [];
        }
    }
    var menus = new Menus([
        {name: '开始游戏' , height: 60 , action: 'play' , hover: {
            bg: 'green',
            stroke: 'green',
        }},
        {
            name: '游戏说明' , height: 40 , action: 'explain' , 
            hover: {
                bg: 'red',
                stroke: 'red'
            },
            radius: [5,5,5,5],
            textSize: 20
        }
    ]);

    menus.onClick(function(item){
        if(item.action == 'play'){
            this.parent.parent.setScene('GamePage');
        }
        if(item.action == 'explain'){
            this.parent.components.hide('IndexMenu');
            this.parent.components.show('Explain');
            this.parent.components.show('CloseExplainButton');
        }
    })

    var menus1 = new Menus([
        {name: '我知道了' , height: 60 , action: 'yes' , hover: {
            bg: 'green',
            stroke: 'green',
        }}
    ]);

    menus1.onClick(function(item){
        if(item.action == 'yes'){
            this.parent.components.show('IndexMenu');
            this.parent.components.hide('Explain');
            this.parent.components.hide('CloseExplainButton');
        }
    })

    indexPage.components.add('IndexBackground' , indexBgComponent);
    indexPage.components.add('IndexMenu' , menus);
    indexPage.components.add('Explain' , {
        draw: function(page){
            textAlign(LEFT , TOP);
            noStroke();
            fill('#fff');
            textSize(14);
            text([
                '游戏玩法及规则如下:',
                "1、使用键盘左右键控制小船的方向移动;",
                "2、小船接到非红色食物时,加2秒游戏时长,并增加对应数字分数;",
                "3、小船接到红色食物时,减少4秒游戏时长,并减少对应数字分数;",
                "4、当游戏时长为0时,游戏结束;",
                "5、游戏开始时,初始时长为20秒;"
            ].join('\n') , 100 , 100 , 200 , 100);
        }
    } , false);
    indexPage.components.add('CloseExplainButton' , menus1 , false)
    return indexPage;
}

function start(games){

    var startPage = new GameSceen();
    startPage.bgColor = 220;

    startPage.components.add('Counts' , {
        score: 0,
        state: 'Ready',
        init: function(){
            this.time = 0;
            this.state = 'Ready';
        },
        draw: function(page){
            fill('blue');
            noStroke();
            textSize(30);
            textAlign(CENTER , CENTER);
            let t = this.score;
            text(t, page.width - textWidth(t) - 20 , 0 , textWidth(t) , 40);
        }
    } , false)

    startPage.components.add('Timer' , {
        time: 20,
        _startTime: 0,
        _currentTime: 0,
        draw: function(page){
            let counts = this.parent.components.get('Counts');
            if(counts.state == 'Play'){
                this._currentTime = page.parent.times.time;
                if(this._startTime == 0){
                    this._startTime = this._currentTime;
                }
                if(this.time - (this._currentTime - this._startTime) <= 0){
                    games.setScene('GameOverPage' , {
                        score: counts.score
                    });
                }else{
                    fill('red');
                    noStroke();
                    textSize(30);
                    textAlign(CENTER , CENTER);
                    let t = this.time - (this._currentTime - this._startTime);
                    text(t, 20 , 0 , textWidth(t) , 40);
                }
            }
        },
        init: function(page){
            this.time = 20;
            this._currentTime = 0;
            this._startTime = 0;
        }
    } , false)

    function Box(){
        this._events = new GameEvents();
        this.init();
    }
    Box.prototype.init = function(page){
        this.width = 60;
        this.x = 0;
        this.height = 40;
        this.bottom = 10;
        this.stroke = '#000';
        this.fill = '#fff';
        this.strokeWeight = 3;
        this.score = 0;
        if(page){
            this.x = (page.width - this.width) / 2;
            this.y = (page.height - this.bottom - this.height);
        }
    }
    Box.prototype.draw = function(page){
        let g = page.parent;
        fill(this.fill);
        stroke(this.stroke);
        strokeWeight(this.strokeWeight);
        beginShape();
        vertex(this.x , page.height - (this.bottom + this.height));
        vertex(this.x + 10 , page.height - (this.bottom));
        vertex(this.x + (this.width - 10) , page.height - this.bottom);
        vertex(this.x + this.width , page.height - (this.bottom + this.height));
        endShape();
        if(g.events.keyIsPressed){
            if(g.events.keyCode == 37 || g.events.keyCode == 39){
                this._events.emit({
                    name: 'keyDown',
                    args: [g.events.keyCode == 37 ? 'left' : 'right'],
                    self: this
                })
            }
        }
    }
    Box.prototype.onKeyDown = function(callback){
        this._events.addEvent('keyDown' , callback);
    }

    var box1 = new Box();

    box1.onKeyDown(function(direction){
        console.log(direction);
        if(direction == 'left'){
            if(this.x > 0){
                this.x -= this.parent.width / 100;
            }else{
                this.x = 0;
            }
        }else{
            if(this.x < (this.parent.width - this.width)){
                this.x += this.parent.width / 100;
            }else{
                this.x = (this.parent.width - this.width)
            }
        }
    })

    startPage.components.add('Box' , box1 , false);

    startPage.components.add('Food' , {
        foods: [],
        draw: function(page){
            if(page.parent.times.continued >= 500){
                this.foods.push({
                    type: floor(random(0,2)),
                    x: floor(random(10 , page.width - 10)),
                    y: 0,
                    speed: floor(random(2 , 8)),
                    size: floor(random(12 , 18))
                })
                page.parent.times.continued = 0;
            }
            let foods = this.foods;
            let foodBox = this.parent.components.get('Box');
            let timer = this.parent.components.get('Timer');
            let counts = this.parent.components.get('Counts');

            for(let i = foods.length - 1; i >= 0; i--){
                let item = foods[i];
                item.y += item.speed;
                fill(item.type == 0 ? '#000' : 'red');
                noStroke();
                circle(item.x , item.y , item.size);
                textSize(10);
                fill('#fff');
                textAlign(CENTER , CENTER);
                text(item.size - 3 , item.x , item.y ,)
                if(item.x >= foodBox.x && item.x < foodBox.x + foodBox.width){
                    if(item.y >= foodBox.y && item.y < foodBox.y + foodBox.height){
                        if(item.type == 0){
                            timer.time += 1;
                            counts.score += item.size - 3;
                        }else{
                            timer.time -= 5;
                            counts.score -= item.size - 3;
                        }
                        foods.splice(i , 1);
                    }
                }
                if(item.y > page.height){
                    foods.splice(i , 1);
                }
            }
        },
        init: function(){
            this.foods = [];
        }
    } , false);

    startPage.components.add('Ready' , {
        startTime: 0,
        time: 3,
        draw: function(page){
            let currentTime = page.parent.times.time;
            let time = 3 - (currentTime - this.startTime);
            noStroke();
            fill('#000');
            textSize(100);
            textAlign(CENTER , CENTER);
            if(time > 0){
                text(time , 0 , 0 , page.width , page.height);
            }else{
                let counts = this.parent.components.get('Counts');
                page.components.hide('Ready');
                page.components.show('Box');
                page.components.show('Food');
                page.components.show('Counts');
                page.components.show('Timer');
                counts.state = 'Play';
            }
        },
        init: function(page){
            this.startTime = page.parent.times.time;
            this.time = 3;
        }
    } , true);

    return startPage;
}

function gameOver(games){
    let gameOver = new GameSceen();

    gameOver.components.add('Counts' , {
        score: 0,
        init: function(page , opts){
            this.score = 0;
            if(opts){
                this.score = opts.score;
            }
        },
        draw: function(page){

            textSize(24);
            fill('red');
            noStroke();
            textAlign(CENTER , CENTER)
            text('游戏结束' , (page.width) / 2 , 200);

            textSize(16);
            fill('#fff');
            noStroke();
            textAlign(CENTER , CENTER)
            text('得分' , (page.width) / 2, 230);

            textSize(30);
            fill('blue');
            noStroke();
            textAlign(CENTER , CENTER)
            text(this.score , (page.width) / 2 , 260);
        }
    });

    var menus = new Menus([
        {name: '再来一次' , height: 60 , action: 'play' , hover: {
            bg: 'green',
            stroke: 'green',
        }},
        {
            name: '返回首页' , height: 40 , action: 'backHome' , 
            hover: {
                bg: 'blue',
                stroke: 'blue'
            },
            radius: [5,5,5,5],
            textSize: 20
        }
    ]);

    menus.onClick(function(item){
        if(item.action == 'play'){
            this.parent.parent.setScene('GamePage');
        }
        if(item.action == 'backHome'){
            this.parent.parent.setScene('IndexPage');
        }
    })

    gameOver.components.add('Menu' , menus);

    return gameOver;
}

function setup() {

    games = new Games({
        width: 600,
        height: 600
    });

    games.addScene('IndexPage' , index(games) , true);
    games.addScene('GamePage' , start(games) , false);
    games.addScene('GameOverPage' , gameOver(games) , false);

    games.start();
}


function draw() {
    games.currentFps = frameRate();
    games.draw();
}
* {
  margin: 0px;
}
body{
    padding: 10px;
}
canvas{
    border: 1px solid red;
}
console 命令行工具 X clear

                    
>
console