Lua Bindings で ScrollView 的な物を実装可能にするためにしたこと


みなさん、おはにちばんは。
この記事は、スクロールした時に他のリスナーの観測をキャンセルさせて、
タッチを独占する処理を Lua でやるために行った事をメモしたものです。
*ScrollView の実装例はここでは解説していません。*
タイトル詐欺ですが、これを応用すれば Lua のみで ScrollView の実装もできる様になるはずです。

#C++で書いた時との比較
C++ 上の cocos2d-x 3.x で実現する方法を通して、
Lua Bindings に必要な機能を考えてみます。

まず、C++ で ScrollView を書こうとした場合は以下のとおりになると思います。

```cpp:TouchThief.cpp
// private std::array<bool, 16> isTouchStolen;
// private cocos2d::EventListenerTouchOneByOne *touchListener;

bool TouchThief::init()
{
	if (!cocos2d::Layer::init()) return false;
	
	// イベントリスナーの登録
	touchListener = cocos2d::EventListenerTouchOneByOne:create();
	touchListener->onTouchBegan = CC_CALLBACK_2(TouchThief::onTouchBegan, this);
	touchListener->onTouchMoved = CC_CALLBACK_2(TouchThief::onTouchMoved, this);
	cocos2d::Director::getInstance()->getEventDispatcher()
	->addEventListenerWithSceneGraphPriority(touchListener, this);
}

void TouchThief::onTouchBegan(Touch *touch, Event *event)
{
	// まだタッチを盗んでいない
	isTouchStolen[touch->getId()] = false;
}

void TouchThief::onTouchMoved(Touch *touch, Event *event)
{
	// 8px 以上動いたらタッチを盗む
	constexpr float threshold = 8.0f * 8.0f;
	
	// まだタッチを盗んでいない状態であるか
	if (!isTouchStolen[touch->getId()]) {
		// タッチの起点からの移動量
		auto startPos = touch->getStartLocationInView();
		auto currentPos = touch->getLocationInView();
		auto delta = currentPos - startPos;
		
		// 十分に動いている
		if (delta.lengthSquared() >= threshold) {
			// キャンセルイベントを生成
			cocos2d::EventTouch eventTouch;
			eventTouch.setEventCode(cocos2d::EventTouch::EventCode::CANCELLED);
			eventTouch.setTouches(std::vector<cocos2d::Touch*>{touch});
			
			// 自分以外にキャンセルイベントを送出
			touchListener->setEnabled(false);
			cocos2d::Director::getInstance()->getEventDispatcher()->dispatchEvent(&eventTouch);
			touchListener->setEnabled(true);
		
			isTouchStolen[touch->getId()] = true;
		}
	}
}

```

これを素直に lua で書く場合はこうなると思います。

```lua:TouchThief.lua
-- CC_CALLBACK_* の代わり
local function selector(thiz, func)
	local pair = setmetatable({ thiz, func }, { __mode = "v"})
	return function(...)
		if pair[1] ~= nil and pair[2] ~= nil then
			return pair[2](pair[1], ...)
		end
	end
end

local TouchThief = class("TouchThief", function() return cc.Layer:create() end)

-- 8px 以上動いたらタッチを盗む
TouchThief.TOUCH_THRESHOLD = 8.0 * 8.0

function TouchThief:ctor()
	self.isTouchStolen = {}
	
	-- イベントリスナーの登録
	local touchListener = cc.EventListenerTouchOneByOne:create() 
	touchListener:registerScriptHandler(selector(self, self.onTouchBegan), cc.Handler.EVENT_TOUCH_BEGAN)
	touchListener:registerScriptHandler(selector(self, self.onTouchMoved), cc.Handler.EVENT_TOUCH_MOVED)
	cc.Director:getInstance():getEventDispatcher():addEventListenerWithSceneGraphPriority(
		touchListener, self
	)
	self.touchListener = touchListener
end

function TouchThief:onTouchBegan(touch, event)
    -- まだタッチを盗んでいない
    self.isTouchStolen[touch:getId() + 1] = false
end

function TouchThief:onTouchMoved(touch, event)
	-- まだタッチを盗んでいない状態であるか
	local touchId = touch:getId() + 1
	if not self.isTouchStolen[touchId]) then
		-- タッチの起点からの移動量
		local startPos = touch:getStartLocationInView()
		local currentPos = touch:getLocationInView()
		local delta = cc.p(currentPos.x - startPos.x, currentPos.y - startPos.y)
		if delta.x * delta.x + delta.y * delta.y >= TouchThief.TOUCH_THRESHOLD then
			-- キャンセルイベントを生成
			local eventTouch = cc.EventTouch:new()
			eventTouch:setEventCode(3) -- cocos2d::EventTouch::EventCode::CANCELLED
			eventTouch:setTouches({touch}) -- <- !!!
			
			-- 自分以外にキャンセルイベントを送出
			self.touchListener:setEnabled(false)
			cc.Director:getInstance():getEventDispatcher():dispatchEvent(eventTouch)
			self.touchListener:setEnabled(true)
			
			self.isTouchStolen[touchId] = true
		end
	end
end

```

これで動いて欲しいのですが、動きません。
理由は cc.EventTouch に setTouches が無いためです。
困りました。

#そうだ cc.EventTouch に get/setTouches() を追加しよう
そこで、Lua Bindings を **直接** 自分で書いてしまいます。
今回は lua_cocos2dx_manual.cpp に書き込みました。
(たぶん、直接 extendEventTouch() に getLuaState() を渡しても動くと思うので、わけても良いと思います。)
まずは以下のコードを `register_all_cocos2dx_manual()` の前に記述します。

```cpp:lua_cocos2dx_manual.cpp
/**
 * cc.Touch:getTouches() 用のバインディング関数
 */
static int lua_cocos2dx_EventTouch_getTouches(lua_State* tolua_S)
{
	int argc = 0;
	cocos2d::EventTouch* cobj = nullptr;
	
	#if COCOS2D_DEBUG >= 1
		// self が cc.EventTouch か確認
		tolua_Error tolua_err;
		if (!tolua_isusertype(tolua_S, 1, "cc.EventTouch", 0, &tolua_err)) goto tolua_lerror;
	#endif
	
	cobj = reinterpret_cast<cocos2d::EventTouch*>(tolua_tousertype(tolua_S, 1, 0));
	#if COCOS2D_DEBUG >= 1
		// 対応する C++ オブジェクトがなかった場合
		if (!cobj) {
			tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_EventTouch_getTouches'", nullptr);
			return 0;
		}
	#endif
	
	// 引数の数を取得
	argc = lua_gettop(tolua_S) - 1;
	
	// 0 個以外の引数の数は受け付けない
	if (argc != 0) {
		luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d\n ", "cc.EventTouch:getTouches", argc, 0);
		return 0;
	}
	
	{
		auto &touches = cobj->getTouches();
		
		// table を作成して 1 つずつ cocos2d::Touch に対応する luaval を詰めていく
		lua_createtable(tolua_S, (int)touches.size(), 0);
		int table = lua_gettop(tolua_S);
		int index = 1;
		for (auto it = std::begin(touches); it != std::end(touches); ++it, ++index) {
			object_to_luaval(tolua_S, "cc.Touch", *it);
			lua_rawseti(tolua_S, table, index);
		}
		return 1;
	}
	
	#if COCOS2D_DEBUG >= 1
		tolua_lerror:
		tolua_error(tolua_S,"#ferror in function 'lua_cocos2dx_EventTouch_getTouches'.",&tolua_err);
		return 0;
	#endif
}

/**
 * cc.Touch:setTouches() 用のバインディング関数
 */
static int lua_cocos2dx_EventTouch_setTouches(lua_State* tolua_S)
{
	int argc = 0;
	cocos2d::EventTouch* cobj = nullptr;
	
	#if COCOS2D_DEBUG >= 1
		// self が cc.EventTouch か確認
		tolua_Error tolua_err;
		if (!tolua_isusertype(tolua_S, 1, "cc.EventTouch", 0, &tolua_err)) goto tolua_lerror;
	#endif
	
	cobj = reinterpret_cast<cocos2d::EventTouch*>(tolua_tousertype(tolua_S, 1, 0));
	#if COCOS2D_DEBUG >= 1
		// 対応する C++ オブジェクトがなかった場合
		if (!cobj) {
			tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_EventTouch_setTouches'", nullptr);
			return 0;
		}
	#endif
	
	// 引数の数を取得
	argc = lua_gettop(tolua_S) - 1;
	
	// 1 個以外の引数の数は受け付けない
	if (argc != 1) {
		luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d\n ", "cc.EventTouch:setTouches", argc, 1);
		return 0;
	}
	
	{
		std::vector<cocos2d::Touch*> touches;
		// 指定された引数が table かチェック
		if (tolua_istable(tolua_S, 2, 0, nullptr)) {
		    // 1 つずつ luaval を対応する cocos2d::Touch* に変換して vector に詰める
			std::size_t length = lua_objlen(tolua_S, 2);
			touches.resize(length);
			for (std::size_t i = 0; i < length; i++) {
				lua_pushnumber(tolua_S, i + 1);
				lua_gettable(tolua_S, 2);
				if (!tolua_isusertype(tolua_S, -1, "cc.Touch", 0, nullptr)) {
					luaL_error(tolua_S, "%s has wrong a argument.\n ", "cc.EventTouch:setTouches");
					return 0;
				}
				luaval_to_object(tolua_S, -1, "cc.Touch", &touches[i], "cc.EventTouch:setTouches");
			}
		}
		
		// cocos2d::Touch* を詰めた vector を渡す
		cobj->setTouches(touches);
		return 1;
	}
	
	#if COCOS2D_DEBUG >= 1
		tolua_lerror:
		tolua_error(tolua_S,"#ferror in function 'lua_cocos2dx_EventTouch_setTouches'.",&tolua_err);
		return 0;
	#endif
}

/**
 * cc.Touch の機能を拡張する関数
 */
static void extendEventTouch(lua_State* tolua_S)
{
	lua_pushstring(tolua_S, "cc.EventTouch");
	lua_rawget(tolua_S, LUA_REGISTRYINDEX);
	if (lua_istable(tolua_S,-1))
	{
		tolua_function(tolua_S, "getTouches", lua_cocos2dx_EventTouch_getTouches);
		tolua_function(tolua_S, "setTouches", lua_cocos2dx_EventTouch_setTouches);
	}
	lua_pop(tolua_S, 1);
}

```

次に `register_all_cocos2dx_manual()` に以下の行を追加します。

```cpp:lua_cocos2dx_manual.cpp
int register_all_cocos2dx_manual(lua_State* tolua_S)
{
	if (NULL == tolua_S)
		return 0;
	
	extendNode(tolua_S);
	//...
	//...
	
	extendEventTouch(tolua_S);  // <- この行を追加
	
	return 0;
}
```

これで、冒頭に書いた lua コードも問題なく動くようになりました。