本文通过解析 dota_addons/ui_example里面的inventory_item控件来说明如何在dota2rpg的UI中制作一个可拖动的控件。
一、可拖动控件的注册和回调的声明
在dota2中,任何的panel都可以变为可拖动的元素,需要在定义这个panel的时候,将这个panel的draggable
tag设置为true。即:
这个panel就可以变为可拖动的panel。
他们就可以通过$.RegisterEventHandler( 'DragEnter', panel, OnDragEnter );
这类语句来绑定他们的拖动回调函数。
这些的事件包括:
$.RegisterEventHandler( 'DragEnter', panel, OnDragEnter );
$.RegisterEventHandler( 'DragDrop', panel, OnDragDrop );
$.RegisterEventHandler( 'DragLeave', panel, OnDragLeave );
$.RegisterEventHandler( 'DragStart', panel, OnDragStart );
$.RegisterEventHandler( 'DragEnd', panel, OnDragEnd);
当然,这也不代表你一定需要定义这么多东西,如果你只需要响应DragStart和DragEnd,那么根据你的实际需要去写即可。
二、拖动回调函数
拖动具体需要哪些函数需要根据制作的实际需要去写。在dota2的可拖动的物品中,需要的函数包括:
OnDragStart:当拖动开始的时候->需要隐藏物品的提示,创建拖动的图像,然后给拖动开始的格子添加一个是从这里开始拖动的显示效果。
OnDragLeave:当拖动着panel的鼠标移动离开panel的时候(这个离开可能是初始格子也可以是中途某个经过的格子)->取消这个格子的高亮状态
DragEnter: 当拖动着的panel鼠标移动到panel上方的时候->高亮这个格子来表示你想要拖动东西到这个格子里;
OnDragDrop:当拖动着panel的鼠标移动到panel上方并松开的时候->你想要交换拖动的原始格子和目标格子中的物品(目标格子可能为空);
OnDragEnd:当拖动着panel的鼠标松开的时候-> 无论是拖动到另一个格子上还是拖动到地上了,都会调用这个回调,因此需要在OnDragDrop
里面对拖动到另一个格子里的事件做出响应。
具体的回调函数如下:
function OnDragStart( panelId, dragCallbacks )
{
// m_Item是在全局里保存的当前物品的id,如果不存在则不开始拖动
if ( m_Item == -1 )
{
return true;
}
// 获取物品名称
var itemName = Abilities.GetAbilityName( m_Item );
// 隐藏正在显示的物品提示
ItemHideTooltip();
// 创建用来拖动的那个panel,也就是被拖动的图像,他并不是原来显示在那里的那个物品图标,而是新建的
var displayPanel = $.CreatePanel( "DOTAItemImage", $.GetContextPanel(), "dragImage" );
// 在创建的那个panel里面保存一些数据,在其他的回调中可以用
displayPanel.itemname = itemName; // 物品名称
displayPanel.contextEntityIndex = m_Item; // 物品序列号ID
displayPanel.data().m_DragItem = m_Item; // 拖动的物品ID,其实这个data()并不是必要的,甚至这一步指令也不是必要的
displayPanel.data().m_DragCompleted = false; // 拖动到底是拖动到另一个格子还是拖动到地上的标志位
// 将用来显示的panel赋值给dragCallbacks.displayPanel就可以开始跟随鼠标移动了
// 这两个offset定义了这个panel到底要在粘上去的位置产生多少偏移
// 因为displayPanel的创建时在$.GetContextPanel,也就是inventory_item的xml里面创建的,因此也就不需要偏移了
dragCallbacks.displayPanel = displayPanel;
dragCallbacks.offsetX = 0;
dragCallbacks.offsetY = 0;
// 这个class会隐藏当前格子的物品图标,就给玩家感觉像是当前的格子里的物品被拖走了
// 其实他还在原来的位置,只不过被隐藏了
// 被拖动的是上面创建的那个displayPanel
$.GetContextPanel().AddClass( "dragging_from" );
return true;
}
function OnDragLeave( panelId, draggedPanel )
{
// 获取正在拖动的物品的ID
var draggedItem = draggedPanel.data().m_DragItem;
// 如果正在拖动的就是当前的物品(也就是第一次离开原始的格子,那么不做响应)
if ( draggedItem === null || draggedItem == m_Item )
return false;
// 如果是拖动经过了这个格子,因为OnDragEnter添加了这个class
// 用来指示这个是当前的目标,离开的时候需要移除这个class
$.GetContextPanel().RemoveClass( "potential_drop_target" );
return true;
}
function OnDragEnter( a, draggedPanel )
{
// 获取正在拖动的物品ID
var draggedItem = draggedPanel.data().m_DragItem;
// 如果正在拖动的就是当前的物品(也就是第一次离开原始的格子,那么不做响应)
if ( draggedItem === null || draggedItem == m_Item )
return true;
// 拖动经过这个格子了,高亮他,用来指示玩家想要拖动物品到这个格子
$.GetContextPanel().AddClass( "potential_drop_target" );
return true;
}
// 这个是关键的回调,因为这个回调里有发送给服务器的交换物品的API
function OnDragDrop( panelId, draggedPanel )
{
// 获取拖动的物品ID
var draggedItem = draggedPanel.data().m_DragItem;
// 如果正在拖动的就是当前的物品(也就是第一次离开原始的格子,那么不做响应)
// 只需要标志一下m_DragCompleted给OnDragEnd使用一下,让他不要
// 丢物品到地上
if ( draggedItem === null )
return true;
draggedPanel.data().m_DragCompleted = true;
if ( draggedItem == m_Item ) // 拿起来又放到同一个格子里面
return true; // 返回,不做交换物品的操作
// 告知服务器玩家想要交换物品
var moveItemOrder =
{
OrderType: dotaunitorder_t.DOTA_UNIT_ORDER_MOVE_ITEM,
TargetIndex: m_ItemSlot,
AbilityIndex: draggedItem
};
Game.PrepareUnitOrders( moveItemOrder );
return true;
}
// 拖动结束,这个里面有丢物品到地上的API
function OnDragEnd( panelId, draggedPanel )
{
// 这个方法无论是拖动到另一个格子里,还是拖动到地上,都会响应
// 如果调用这个之前没有先调用OnDragDrop的话,那么就告知
// 服务器玩家想要丢东西到地上
if ( !draggedPanel.data().m_DragCompleted )
{
Game.DropItemAtCursor( m_QueryUnit, m_Item );
}
// 移除被拖动的面板
draggedPanel.DeleteAsync( 0 );
// 清除拖动来源的格子,也就是调用这个的格子的dragging_from效果
// UI里的交换物品图标,则会在服务器完成调换格子的操作之后
// 通知客户端更新
$.GetContextPanel().RemoveClass( "dragging_from" );
return true;
}
三、其他的拖动需求分析
- 可拖动的窗口,例如
背包窗口
等,那么OnDragEnter、OnDragDrop和OnDragLeave是不需要的,应该只需要OnDragStart和OnDragEnd,此外,由于没有兄弟panel的问题,因此displayPanel
可以使用窗口本体,这样你甚至连OnDragEnd都不需要写具体的内容。 - 卡牌游戏的卡牌,卡牌游戏的卡牌,也只需要OnDragStart和OnDragEnd,此外,卡牌在拖动过程中的响应,需要自己使用Schedule不断更新,比如更新卡牌的当前状态,指示当前卡牌目标位置等。