Egret基础-事件与自定义事件

游戏中的事件机制

在游戏开发过程中,我们会不断的使用事件的机制,不管是Touch操作还是网络通信都离不开这些,本篇我们就事件机制做一个整理和学习。

cocos2d-x中的事件机制

使用过cocos2d-x的朋友都知道,其事件机制主要由以下几个类控制
CCEvent CCEventController CCEventCustom CCEventDispatcherCCEventListenerCCEventType
如果你对cocos2d-x事件了解的非常好,对于Egret中的事件你也会非常容易理解。为了后面的加深理解,先简单的阐述下cocos2d-x中事件的原理。
cocos2d-x中的Event类型大概分为:

enum class Type
{
    TOUCH,
    KEYBOARD,
    ACCELERATION,
    MOUSE,
    FOCUS,
    GAME_CONTROLLER,
    CUSTOM
 };

而Egret中的事件类型,看目录egret\events,我们知道Egret默认事件的类型包括
Focus(焦点)Geolocation(定位)HTTPSIOErrorMotion(手势)Orientation(旋转与方向)ProgressStageOrientationTextTimerTouch
在cocos2d-x中,不同的事件使用不同的事件监听器的创建接口,当然也存在自定义的事件监听器接口。我们这里均以 Touch 和 Custom 来进行举例说明。
这里我们以cocos2d-x的EventListenerTouchOneByOne举例

class CC_DLL EventListenerTouchOneByOne : public EventListener
{
public:
    ...
    static EventListenerTouchOneByOne* create();
    ...
public:
    typedef std::function<bool(Touch*, Event*)> ccTouchBeganCallback;
    typedef std::function<void(Touch*, Event*)> ccTouchCallback;
    ccTouchBeganCallback onTouchBegan;
    ccTouchCallback onTouchMoved;
    ccTouchCallback onTouchEnded;
    ccTouchCallback onTouchCancelled;
    ...
};
// CPP
EventListener::init(Type::TOUCH_ONE_BY_ONE, LISTENER_ID, nullptr)

从上述代码我们知道主要执行了父类的init方法,其实主要就是将事件回调、事件类型、事件ID等信息进行了初始化。而添加事件监听的地方在 EventDispatcher ,对应添加事件监听和删除事件监听大致有这几个方法:


// .h
void addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node);
void addEventListenerWithFixedPriority(EventListener* listener, int fixedPriority);
EventListenerCustom* addCustomEventListener(const std::string &eventName, const std::function<void(EventCustom*)>& callback);
void removeEventListener(EventListener* listener);
void removeEventListenersForType(EventListener::Type listenerType);
void removeEventListenersForTarget(Node* target, bool recursive = false);
void removeCustomEventListeners(const std::string& customEventName);
void removeAllEventListeners();

事件这块都涉及了优先级的概念。同样的在Egret中事件分发接口 IEventDispatcher 中有如下定义:


/**
* @language zh_CN
* 使用 EventDispatcher 对象注册事件侦听器对象,以使侦听器能够接收事件通知。可以为特定类型的事件、阶段和优先级在显示列表的所有节
* 点上注册事件侦听器。成功注册一个事件侦听器后,无法通过额外调用 on() 来更改其优先级。要更改侦听器的优先级,必须
* 先调用 removeEventListener()。然后,可以使用新的优先级再次注册该侦听器。注册该侦听器后,如果继续调用具有不同 type 或 useCapture
* 值的 on(),则会创建单独的侦听器注册。<br/>
* 如果不再需要某个事件侦听器,可调用 EventDispatcher.removeEventListener()
* 删除它;否则会产生内存问题。由于垃圾回收器不会删除仍包含引用的对象,因此不会从内存中自动删除使用已注册事件侦听器的对象。复制
* EventDispatcher 实例时并不复制其中附加的事件侦听器。(如果新近创建的节点需要一个事件侦听器,必须在创建该节点后附加该侦听器。)
* 但是,如果移动 EventDispatcher 实例,则其中附加的事件侦听器也会随之移动。如果在正在处理事件的节点上注册事件侦听器,则不会在当
* 前阶段触发事件侦听器,但会在事件流的稍后阶段触发,如冒泡阶段。如果从正在处理事件的节点中删除事件侦听器,则该事件侦听器仍由当前操
* 作触发。删除事件侦听器后,决不会再次调用该事件侦听器(除非再次注册以备将来处理)。
* @param type 事件的类型。
* @param listener 处理事件的侦听器函数。此函数必须接受 Event 对象作为其唯一的参数,并且不能返回任何结果,
* 如下面的示例所示: function(evt:Event):void 函数可以有任何名称。
* @param thisObject 侦听函数绑定的this对象
* @param useCapture 确定侦听器是运行于捕获阶段还是运行于冒泡阶段。如果将 useCapture 设置为 true,
* 则侦听器只在捕获阶段处理事件,而不在冒泡阶段处理事件。如果 useCapture 为 false,则侦听器只在冒泡阶段处理事件。
* 要在两个阶段都侦听事件,请调用 on() 两次:一次将 useCapture 设置为 true,一次将 useCapture 设置为 false。
* @param  priority 事件侦听器的优先级。优先级由一个带符号的整数指定。数字越大,优先级越高。优先级为 n 的所有侦听器会在
* 优先级为 n -1 的侦听器之前得到处理。如果两个或更多个侦听器共享相同的优先级,则按照它们的添加顺序进行处理。默认优先级为 0。
* @see #once()
* @see #removeEventListener()
* @version Egret 2.4
* @platform Web,Native
*/
addEventListener(type:string, listener:Function, thisObject:any, useCapture?:boolean, priority?:number):void;
...
once(type:string, listener:Function, thisObject:any, useCapture?:boolean, priority?:number):void;
removeEventListener(type:string, listener:Function, thisObject:any, useCapture?:boolean):void;

文件中的注释也非常清楚,从这个角度说Egret和cocos2d-x的事件使用方法上非常类似。至于原理,我一会儿会在Egret中讲解,在解释cocos2d-x事件原理之前,我们知道有了addEventListener,还有一个不得不说的就是dispatch。dispatch就是分发的意思,意思我们单单addEventListener之后,事件只是被注册,但是什么时候去调用,还是需要我们使用dispatch来分发消息的。在Egret中也是同样的道理。因为本文库主要阐述的是Egret的方方面面,所以其他引擎我会粗略的发表一下自己的意见,有兴趣的朋友可以私下多多交流。
addEventListener 所做的事情就是就是将注册的事件加入到_listenerMap中,而 removeEventListener 的时候会将事件从_listenerMap中删除。在dispatchEvent的时候,首先会根据优先级对时间监听进行排序,然后执行代码如下:


auto onEvent = [&event](EventListener* listener) -> bool{
    event->setCurrentTarget(listener->getAssociatedNode());
    listener->_onEvent(event);
    return event->isStopped();
};

我们整体寻找一下默认的dispatch有哪些?当然自定义的事件需要你自己add之后,进行dispatch喔。

我们搜索了以后得到了很多结果,为了验证找其中一处:
当我们注册了Touch的Listener,在底层点击的时候,已经加入了dispatch,所以我们无需自己调用。
在cocos2d-x这里说了这么多,只有一个意思,因为Egret的事件机制和cocos2d-x基本是一致的。

Egret中的事件机制浅析

事件机制原理(官方):http://edn.egret.com/cn/docs/page/112
我这里简单的做一个说明或者概括:

 所有事件机制的原理大多相同:
 基本都有【事件发送方】、【事件接收方】、【事件】三个部分组成。
 事件是连接 发送方 和 接收方的桥梁。
 事件发送方在发送事件之后可以等待回应或者认为事情已经完成。
 事件接收方接到事件会去根据事件做不同的事情,这个事情我们称之为回调。

有了上面对cocos2d-x的事件分析,我们了解了一些,下面我们就从代码层面开始研究,最后我们会多说几个例子来整体进行巩固。那么我们就从这里开始。


由于文件个数并不多,所以我们依次看看这些文件的父子级关系和内容。
Event.ts


// 文件的开始对这个文件有一个非常好的注释:
/**
Event 类作为创建事件实例的基类,当发生事件时,Event 实例将作为参数传递给事件侦听器。Event 类的属性包含有关事件的基本信息,例如事件的类型或者是否可以取消事件的默认行为。对于许多事件(如由 Event 类常量表示的事件),此基本信息就足够了。但其他事件可能需要更详细的信息。
*/
// 简单点说上面我们提到了事件机制的三个部分,这里就是【事件的基类】

下面我们写个例子,测试怎么创建一个事件。


private EventTest(e: egret.Event) {
    console.log(e.data);
}
...
var e:egret.Event = egret.Event.create(egret.Event, "Test");
e.data = "TestData";

var p:egret.DisplayObjectContainer = new egret.DisplayObjectContainer();
p.addEventListener("Test", this.EventTest, this);

var o:egret.DisplayObjectContainer = new egret.DisplayObjectContainer();

var ret = o.dispatchEvent(e);
console.log(ret);

好吧,我承认上述的代码不漂亮,而且更糟糕的是没有执行,一会儿我们一起看看这个为什么,从这个角度而言也说明,和我们所了解的cocos还是有些区别的。
我们在编译后的代码中

这里得到的list是空。如果我们此时直接让p.dispatchEvent(e)是没有问题的。
上面也说明,一个新的对象直接去dispatchEvent的时候,他的eventMap是没有的。
当然我们先按照官方的设定来玩这个概念。之后了解清楚原理,可以自己随意扩展。
现在我们看看那个地方是最重要的呢?毫无疑问 addEventListener 非常重要。
事件派发器之Add

下面我们就从Egret的事件派发器说起。
EventDispatcher继承于IEventDispatcher
这个接口中的几个方法,需要一个一个详细的聊聊。

addEventListener

参数:
type:string 事件的类型
listener:Function 处理事件的侦听器函数。此函数必须接受 Event 对象作为其唯一的参数,并且不能返回任何结果
thisObject:any 侦听函数绑定的this对象
useCapture?:boolean 确定侦听器是运行于捕获阶段还是运行于冒泡阶段
priority?:number 事件侦听器的优先级
通过分析源码我们知道addEventListener主要是判断事件优先级确定index,然后构建eventBin如下:


var eventBin = {
    type: type, listener: listener, thisObject: thisObject, priority: priority,
    target: this, useCapture: useCapture, dispatchOnce: !!dispatchOnce
};

然后将事件插入到list中,也就说明此时改对象的eventMap就有了对应的事件了。所以该对象在执行dispatchEvent的时候,取得对应的list,然后通过下面一行命令调用回调:
eventBin.listener.call(eventBin.thisObject, event);
说完了这个,再重新写个例子,连带整理下Egret事件使用方法:


// 自定义顾客事件
class CustomerEvent extends egret.Event {

    public static BUY:string = "buy";
    public money:number;
    public constructor(type:string, bubbles:boolean = false, cancelable:boolean = false) {
        super(type, bubbles, cancelable);
    }
}
// 商品类
class Good extends egret.DisplayObject {

    private _name:string;
    private _value:number;
    private _jin:number;

    constructor(name:string, value:number, jin:number) {
        super();
        this._name = name;
        this._value = value;
        this._jin = jin;
    }

    public get name() :string {
        return this._name;
    }

    public set name(v:string) {
        this._name = v;
    }

    public get value():number {
        return this._value;
    }

    public set value(v:number) {
        this._value = v;
    }

    public get jin():number {
        return this._jin;
    }
    public set jin(v:number) {
        this._jin = v;
    }
}

// 商店类
class Shop extends egret.DisplayObject {

    public static totalMoney:number;
    private goods:Array<Good>;
    private config:Array<any> = [
        ["苹果", 5.98, 15],
        ["鸭梨", 4.1, 22],
        ["香蕉", 7.2, 17],
        ["猕猴桃", 14, 5]
    ];
    private static _shopInstance: Shop;
    constructor() {
        super();
    }

    public AddMoney(e:CustomerEvent) {
        Shop.totalMoney = Shop.totalMoney + e.money;
        console.log("ShopMoney:$" + Shop.totalMoney);
    }

    private init():void {
        this.addEventListener(CustomerEvent.BUY, this.AddMoney, this);
        Shop.totalMoney = 0;
        this.goods = new Array<Good>();
        for (var i = 0; i < this.config.length; i++) {
            let name = this.config[i][0];
            let value = this.config[i][1];
            let jin = this.config[i][2];
            this.goods.push(new Good(name, value, jin));
        }
    }

    public static getInstance():Shop {

        if (!Shop._shopInstance) {
            Shop._shopInstance = new Shop();
            Shop._shopInstance.init();
        }
        return Shop._shopInstance;
    }

    public getGoods():Array<Good> {
        return this.goods;
    }

    public getRandomGood() {
        var ret = null;
        var index = Math.floor(Math.random() * this.goods.length);
        if (this.goods[index]) {
            ret = this.goods[index];
        }
        return ret;
    }

    public allGoodOver() {
        var ret = false;
        var is_zero = 0;
        for (var i = 0; i < this.goods.length; i++) {
            is_zero = is_zero + this.goods[i].jin;
        }
        if (is_zero == 0) {
            ret = true;
        }
        return ret;
    }
}

// 顾客类
var family_name:string[] = ["张", "王", "李", "魏", "吴", "袁", "周", "孙"];
var post_name:string[] = ["博", "洋", "秋水", "长苏"];
function genName():string {
    var ret = "";
    var index1 = Math.ceil(Math.random() * family_name.length) - 1;
    var index2 = Math.ceil(Math.random() * post_name.length) - 1;
    ret = family_name[index1] + post_name[index2];
    return ret;
}

class Customer extends egret.DisplayObject {

    private _balance:number;
    private c_name:string;
    constructor() {
        super();
        // 自动名字
        this.c_name = genName();
        console.log("Name:" + this.c_name);
        // 自动余额
        this._balance = Math.floor(Math.random() * 100) + 100;
        console.log("Balance:" + this._balance);
        // 添加事件监听
        this.addEventListener(CustomerEvent.BUY, this.buyCallBack, this);
    }

    // 购买
    public buyCallBack(e:CustomerEvent) {
        this._balance = this._balance - e.money;
        console.log(this.c_name + "剩余:$" + this._balance);
    }

    public set balance(v:number) {
        this._balance = v;
    }

    public get balance():number {
        return this._balance;
    }

    public buy(index:number, n:number) {
        // 购买
        var shop = Shop.getInstance();
        var goods:Array<Good> = shop.getGoods();
        var e:CustomerEvent = new CustomerEvent(CustomerEvent.BUY);
        e.money = goods[index].value * n;

        // 更新数据
        goods[index].jin = goods[index].jin - n;
        shop.dispatchEvent(e);
        this.dispatchEvent(e);
    }
}

class GameMechine extends egret.DisplayObject {
    public static startGame(customers:Array<any>) {

        var shop = Shop.getInstance();
        var max_circle = 100;
        var start_index = 0;
        while (true) {
            // 检测
            if (shop.allGoodOver()) {
                console.log("Game Over1");
                break;
            }

            if (start_index > max_circle) {
                console.log("Game Over2");
                break;
            }

            console.log("Nexting..");
            // 选择钱数最多的
            var p_index:[number, number] = [0, 0];
            for (var i = 0; i < customers.length; i++) {
                let c = <Customer>customers[i];
                if (p_index[1] < c.balance) {
                    p_index[0] = i;
                    p_index[1] = c.balance;
                }
            }

            // 购买价格 * 斤数最高的,如果没有次之
            var g_index:[number, number] = [0, 0];
            var goods:Array<Good> = shop.getGoods();
            for (var i = 0; i < goods.length; i++) {
                var value = goods[i].value * goods[i].jin;
                if (g_index[1] < value) {
                    g_index[0] = i;
                    g_index[1] = value;
                }
            }

            // 默认1
            (<Customer>customers[p_index[0]]).buy(g_index[0], 1);
        }
    }
}
//// 具体执行
var customers:Array<Customer> = new Array<Customer>();
for (var i = 0; i < 3; i++) {
    // 生成三名顾客
    let c = new Customer();
    customers.push(c);
}

console.log("===================Start====================");
GameMechine.startGame(customers);

欢迎留言

avatar
  Subscribe  
Notify of