当前位置: 首页 > 工具软件 > PlayCanvas > 使用案例 >

使用PlayCanvas制作一个简单的小游戏(三)

裴金鑫
2023-12-01

原文:http://developer.playcanvas.com/zh/tutorials/beginner/keepyup-part-three/


游戏脚本和输入

在场景的根实体Game上绑定了两个脚本,game.js和input.js。脚本通常是按照在层级结构中遇到的先后顺序执行的,因此最简单的做法是将所有不与某个实体相关的脚本都绑定到第一个实体上。(注意,你也可以使用脚本优先级对话框设置优先加载的脚本,而无需将其绑定到任何一个实体上)


game.js

pc.script.attribute("uiMenu", "entity", null);
pc.script.attribute("uiInGame", "entity", null);
pc.script.attribute("uiGameOver", "entity", null);
pc.script.attribute("audio", "entity", null);

pc.script.create('game', function (app) {
    var STATE_MENU = "menu";
    var STATE_INGAME = "ingame";
    var STATE_GAMEOVER = "gameover";

    // Creates a new Game instance
    var Game = function (entity) {
        this.entity = entity;

        this._state = STATE_MENU;
    };

    Game.prototype = {
        initialize: function () {
            this._score = 0;

            this.setResolution();

            window.addEventListener("resize", this.setResolution.bind(this));

            // listen to events from the UI
            app.on("ui:start", this.start, this);
            app.on("ui:reset", this.reset, this);
        },

        update: function (dt) {
        },

        setResolution: function () {
            // if the screen width is less than 640
            // fill the whole window
            // otherwise
            // use the default setting

            var w = window.screen.width;
            var h = window.screen.height;

            if (w < 640) {
                app.setCanvasResolution(pc.RESOLUTION_AUTO, w, h);
                app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
            }
        },

        // Call this to move from MENU to INGAME
        start: function () {
            this._state = STATE_INGAME;
            app.fire("game:start");
            this.uiMenu.enabled = false;
            this.uiInGame.enabled = true;

            this.audio.sound.play("music");
        },

        // Call this to move from INGAME to GAMEOVER
        gameOver: function () {
            this._state = STATE_GAMEOVER;
            app.fire("game:gameover");
            this.uiInGame.enabled = false;
            this.uiGameOver.enabled = true;

            this.audio.sound.stop();
            this.audio.sound.play("gameover");
        },

        // Call this to move from GAMEOVER to MENU
        reset: function () {
            app.fire("game:reset");
            this.resetScore();
            this._state = STATE_MENU;
            this.uiGameOver.enabled = false;
            this.uiMenu.enabled = true;

            this.audio.sound.stop();
        },

        // return the current score
        getScore: function () {
            return this._score;
        },

        // add a value to the score
        addScore: function (v) {
            this._score += v;
            app.fire("game:score", this._score);
        },

        // reset the score
        resetScore: function () {
            this._score = 0;
            app.fire("game:score", this._score);
        }
    };

    return Game;
});

游戏状态

game脚本管理了游戏的全部状态,同时提供了一些方法来改变游戏的状态,并且处罚事件来通知其他的代码游戏的状态已经改变。

我们将游戏的状态分为三种:菜单态、游戏中和游戏结束。游戏脚本提供了状态切换的方法,如start(),gameOver()和reset()。每个方法将当前的状态记录在_state变量中,发送一个事件通知其他的代码脚本状态已改变;切换用户界面上的开关;管理音乐的状态和游戏结束时的音效。

当特定的触发事件产生的时候,这些状态切换方法将被其他脚本代码调用。比如,当球跌出屏幕下方时,ball.js将会调用gameOver()方法。


应用程序事件

我们来看看游戏脚本是如何在程序中发送事件的。

app.fire("game:start")
对于在脚本之间通信来说,事件是一种极其有用的方法。事件工作的方法是一个对象(这里是app)选择来“发送”一个事件。所有访问这个对象的代码都可以选择侦听一个或多个这个对象的事件,当事件被发送的时候,这些代码将会得到通知。

这里的一个问题是,代码需要先访问这个对象才能开始监听它的事件。这就是为什么应用程序事件是如此有用的原因了。正是由于所有的脚本在创建的时候都会传入app这个变量,所以PlayCanvas的每个脚本都能够访问app。这使得app成为脚本之间通信的枢纽。

我们得选择一种命名空间的规则来让事件变得更加清晰,从而避免冲突。要监听game:start事件,需要如下代码:

app.on("game:start", function () {
    console.log("game:start event was fired");
}, this)

计分

游戏脚本还负责记录当前的得分。它提供了修改得分和发送事件的方法,通过这一方法其他代码可以知道得分被修改了。

分辨率

最后,游戏脚本处理初始的分辨率,以保证主画面的尺寸对于手机和PC来说都是合适的。在移动设备上(屏幕宽度小于640像素)游戏直接占满整个屏幕。在PC上则使用在工程设置中预设的分辨率。

input.js

输入脚本监听来自鼠标或触屏的输入信号,将两种输入归为一种通用的“点击”输入,然后完成当“点击”后的处理。

pc.script.attribute("ball", "entity", null);
pc.script.attribute("camera", "entity", null);
pc.script.attribute("ballRadius", "number", 0.5);

pc.script.create('input', function (app) {
    var PIXEL_SIZE = 30;

    var screenPos = new pc.Vec3();
    var worldPos = new pc.Vec3();

    // Creates a new Input instance
    var Input = function (entity) {
        this.entity = entity;
        this._paused = true;
    };

    Input.prototype = {
        initialize: function () {
            var self = this;

            // Listen for game events so we know whether to respond to input
            app.on("game:start", function () {
                self._paused = false;
            });
            app.on("game:gameover", function () {
                self._paused = true;
            });

            // set up touch events if available
            if(app.touch) {
                app.touch.on("touchstart", this._onTouchStart, this);
            }

            // set up mouse events
            app.mouse.on("mousedown", this._onMouseDown, this);
        },

        _onTap: function (x, y) {
            var p = this.ball.getPosition();
            var camPos = this.camera.getPosition();

            // Get the position in the 3D world of the touch or click
            // Store the in worldPos variable.
            // This position is at the same distance away from the camera as the ball
            this.camera.camera.screenToWorld(x, y, camPos.z - p.z, worldPos);

            // get the distance of the touch/click to the ball
            var dx = (p.x - worldPos.x);
            var dy = (p.y - worldPos.y);

            // If the click is inside the ball, tap the ball
            var lenSqr = dx*dx + dy*dy;
            if (lenSqr < this.ballRadius*this.ballRadius) {
                this.ball.script.ball.tap(dx, dy);
            }
        },

        _onTouchStart: function (e) {
            if (this._paused) {
                return;
            }

            // respond to event
            var touch = e.changedTouches[0];
            this._onTap(touch.x, touch.y);

            // stop mouse events firing as well
            e.event.preventDefault();
        },

        _onMouseDown: function (e) {
            if (this._paused) {
                return;
            }

            // respond to event
            this._onTap(e.x, e.y);
        }
    };

    return Input;
});

首先,我们在initialize中设置事件监听方法。我们通过监听应用程序发出的事件来决定游戏是否处于暂停状态(或菜单态或游戏结束状态)。如果输入暂停了,我们也无需响应那些点击了。接下俩,我们监听触摸事件(注意,你必须检查app.touch是否有效)和鼠标事件。

触摸事件

在触摸事件中,我们第一次点击屏幕时会传入点击屏幕的坐标点。这里还调用了浏览器的preventDefault()方法来禁止浏览器也产生会引发别的响应的点击事件。

鼠标事件

在鼠标按下事件发生时传入了点击点屏幕坐标。注意,PlayCanvas保证了触摸和鼠标事件具有相同的坐标系。而一般的浏览器事件却不是这样的。

触摸

如果触摸点击中了球,则_onTap()会计算屏幕坐标xy,然后告知控制球的代码它被触摸了一下。

this.camera.camera.screenToWorld(x, y, camPos.z - p.z, worldPos);

具体来说,函数获得了屏幕的坐标xy,并让摄像机将位于屏幕下方的点转换为三维空间中的位置。为了实现这一点,我们得需要一个深度信息,也就是你希望这个三维空间中的点距离屏幕多远。这里,我们设置这个三维点与球的深度相同。

我们还传入了一个矢量worldPos,它在PlayCanvas中十分重要,这是因为它用来避免创建新的对象,如同在update循环中调用new pc.Vec3()来创建一个新的矢量一样。内存分配的次数越多(调用new),浏览器清理这些分配所需的垃圾回收的次数就越多。垃圾回收是一种(相对较慢)操作,如果频繁的调用,它会使得你的游戏运行的很卡。

在大多数情况下,PlayCanvas都会提供一个选项来传入一个适量或类似的选项,这样你可以预先分配内存并重复利用对象。

// get the distance of the touch/click to the ball
var dx = (p.x - worldPos.x);
var dy = (p.y - worldPos.y);

// If the click is inside the ball, tap the ball
var lenSqr = dx*dx + dy*dy;
if (lenSqr < this.ballRadius*this.ballRadius) {
    this.ball.script.ball.tap(dx, dy);
}
当我们获得了触摸点在三维空间中的位置,我们就可以检测它是否与球重叠。你可以发现这里我们检测的是半径的平方与触摸点和球之间的距离。这样我们在测试时无须每次都执行较慢的平方根操作。

当触摸点与球重合,就调用球脚本中的tap(dx,dy)方法并传入触摸点与球之间的距离。









 类似资料: