创建摇杆快速菜单

Time:15 分钟

摇杆快速菜单(Radial Menu)采用了模拟纵杆控制器,且易于使用,所以成为了主机游戏中经常出现的一种菜单形式。在摇杆快速菜单中,可将选项都置于一个圆环内。用户推动模拟杆时即可选择所推方向的选项。

本教程内容涵盖操作摇杆快速菜单的方法。虽然鼠标/键盘或手机游戏也可使用相同的方法,但这里我们会重点讲解游戏手柄。

设置菜单

可以手动或通过代码创建菜单。本教程采用代码,所以易于在各类游戏中应用:

构建一个摇杆快速菜单 ```

-- 决定打开和关闭状态下菜单框架的位置和大小
local menuOpenPosition = UDim2.new(0.25, 0, 0.25, 0)
local menuOpenSize = UDim2.new(0.5, 0, 0.5, 0)
local menuClosedPosition = UDim2.new(.5, 0, .5, 0)
local menuClosedSize = UDim2.new(0.005, 0, 0.005, 0)

-- 决定菜单选项的元素
local itemTemplate = Instance.new("TextButton")
itemTemplate.Size = UDim2.new(0.3, 0, 0.2, 0)
itemTemplate.AutoButtonColor = false
itemTemplate.TextScaled = true

-- 为菜单创建屏幕和框架
local menuScreen = Instance.new("ScreenGui", player:WaitForChild("PlayerGui"))
menuScreen.Name = "MenuScreen"
local menuFrame = Instance.new("Frame", menuScreen)
menuFrame.Size = menuClosedSize
menuFrame.Position = menuClosedPosition
menuFrame.Visible = false
menuFrame.BackgroundTransparency = 1
menuFrame.BorderSizePixel = 0
menuFrame.Name = "MenuFrame"
摇杆快速菜单通常具有两种状态:打开和关闭。此代码首先会针对菜单的主框架定义位置和大小。有了当前值,打开状态的菜单将在屏幕上居中显示,宽和高占屏幕的一半。你可以调整 `menuOpenPosition` 和 `menuOpenSize`,用以调整菜单所占屏幕的比例以及在何处居中显示。

接下来,脚本会为菜单项创建一个模板。在本例中,使用 `TextButton` 是因为非常简便。你应当将其更改为使用与游戏和界面相符的更复杂元素。请记住,代码将通过大小变化来动态呈现菜单的打开。因此 `TextButton/TextScaled` 可以确保文本能够随着按钮而动态改变大小。

`AutoButtonColor` 被禁用,因为本教程着重于使用游戏手柄。如果将此代码改为使用鼠标,你应当启用此属性。

最后,代码会创建 `ScreenGui` 和 `Frame`。框架的 `GuiObject/Size` 和 `GuiObject/Position` 将分别设置为 `menuClosedSize` 和 `menuClosedPosition`,因为菜单最初应当为隐藏状态。

## 创建按钮

设置完菜单屏幕和框架,然后脚本就会自动创建圆圈中放置的按钮。你可使用几个常量来配置要显示在圆圈中的按钮数量及其方向。

创建摇杆快速菜单物品 ```    
    
    local RADIUS = .5
    local NUM_OPTIONS = 6
    local ANGLE_OFFSET = 90
    local menuItems = {}
    local function newMenuItem(name, angle, range)
    	local newItem = {}
    	local label = itemTemplate:Clone()
    	label.Text = name
    	label.Name = name
    	local angleRadians = math.rad(ANGLE_OFFSET + angle)
    	label.Position = UDim2.new(.5 + RADIUS * math.cos(angleRadians) - label.Size.X.Scale / 2, 0,
    							 .5 - RADIUS * math.sin(angleRadians) - label.Size.Y.Scale / 2, 0)
    	label.Parent = menuFrame
    	newItem.Label = label
    	newItem.Vector = Vector2.new(math.cos(angleRadians), math.sin(angleRadians))
    	newItem.Range = range
    	table.insert(menuItems, newItem)
    end

newMenuItem 函数用于创建新的菜单项,并将其添加到一表格中,随后在决定选择哪个项目时会用到表格和其中内容。该函数需要三个参数:name 表示选项的名称(也用于在 GUI 中标记它),angle 设置选项在圆环转盘上的中心位置,range 设置输入弧的宽度。

RadialMenu_Image01.png

该函数首先会创建一个先前创建好的 itemTemplate 的克隆副本。它还根据 angle 设置了按钮的位置。 该函数还存储了一个由输入的 angle 构成的 2D 向量。它将随后用于与用户输入进行比较,以确定哪个项目被选中。

for i = 1, NUM_OPTIONS do
	local angle = (360 / NUM_OPTIONS) * (i - 1)
	local name = "Option" .. i
	newMenuItem(name, angle, 360 / NUM_OPTIONS)
end

声明 newMenuItem 函数之后,代码会执行一个循环来创建菜单项。常量 NUM_OPTIONS 决定了要创建多少个菜单选项。每个项目的角度是通过将 360(圆中的度数)除以 NUM_OPTIONS,然后乘以当前选项来确定。另外,范围同样也是由 360 除以 NUM_OPTIONS 来确定。这些值都会被传递到 newMenuItem 中,用以创建按钮。

打开菜单

在此教程中,菜单可通过左操纵杆在打开和关闭之间切换。ContextActionService 可用于将此按钮绑定到能打开和关闭菜单的自定义函数。

-- 菜单和背包都使用左纵杆。禁用背包 UI 以防止冲突
game.StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false)

local function toggleMenu(actionName, inputState)
	if inputState == Enum.UserInputState.Begin then
		openMenu()
	elseif inputState == Enum.UserInputState.End then
		closeMenu()
	end
end

ContextActionService:BindAction("ToggleMenu", toggleMenu, false, Enum.KeyCode.ButtonL1)

默认的 Roblox 控制脚本会将操纵杆与道具包中项之间的开关绑定。要将此输入重新绑定到摇杆快速菜单,则必须使用 StarterGui/SetCoreGuiEnabled 将背包 UI 禁用。如果针对摇杆快速菜单使用其他输入而非操纵杆,则应当注意 Roblox 的默认设置以避免发生冲突。

toggleMenu 函数使用 ContextActionService/BindAction 绑定到左操纵杆。toggleMenu 会查看 inputState 以了解操纵杆处于何种状态。如果是 Begin 状态,那么用户只需开始按操纵杆并打开菜单。否则,如果是 End 状态,则需要关闭菜单。

local function openMenu()
	-- 存储 GuiNavigationEnabled  AutoSelectGuiEnabled 然后将二者设置为 false
	wasGuiNavigationEnabled = GuiService.GuiNavigationEnabled		
	wasAutoSelectGuiEnabled = GuiService.AutoSelectGuiEnabled
	GuiService.GuiNavigationEnabled = false
	GuiService.AutoSelectGuiEnabled = false
	
	-- 绑定 onThumbstickMoved 函数
	ContextActionService:BindAction("RadialMenu", onThumbstickMoved, false, Enum.KeyCode.Thumbstick1)		
	-- 确定框架可见并播放打开动画
	menuFrame.Visible = true
	menuFrame:TweenSizeAndPosition(menuOpenSize, menuOpenPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quart, .5, true)
	menuOpen = true	
end

openMenu 函数需要执行几点事项。首先,需要禁用 GuiService/GuiNavigationEnabledGuiService/AutoSelectGuiEnabled,并存储该值,这样能够便于稍后执行恢复。这些设置适用于其他菜单类型,却不适合于摇杆快速菜单。

你可能会注意到,代码只是除以传入矢量的大小值(操纵杆的位置),而不是除以项矢量的大小值。这是因为项矢量保证为单位矢量,是通过 Vector2.new(math.cos(angle), math.sin(angle)) 计算得出的。单位矢量大小为 1,所以可以从除数中忽略不计。

然后将矢量之间的角度与项范围值的一半做比较。如果小于此值,则表明操纵杆矢量位于针对该项定义的弧的某个位置。如果是这种情形,那么 getButtonFromVector 会返回项按钮。

如果 getButtonFromVector 返回一个按钮,onThumbstickMoved 会将 GuiService/SelectedObject 设置到该按钮。这样会高亮显示按钮,用户就知道已经选择了此按钮。

onThumbstickMoved 的末尾,如果操纵杆无法推过盲区,则会将 SelectedObject 设置为空,以便清除选项。

关闭菜单

菜单关闭后,如果存在已选项,代码需要基于该选项来调用一个函数。

-- 选择物品时将调用的函数。您应把自己的自定义代码放在此处
-- 来实现您自己的菜单
local function onMenuSelect(option)
	print(option, "selected")
end

local function closeMenu()
	-- 恢复 GuiNavigationEnabled 和 AutoSelectGuiEnabled
	GuiService.GuiNavigationEnabled	= wasGuiNavigationEnabled
	GuiService.AutoSelectGuiEnabled = wasAutoSelectGuiEnabled	

	-- 解除绑定 onThumbstickMoved 函数		
	ContextActionService:UnbindAction("RadialMenu")
	
	if GuiService.SelectedObject then
		-- 如果在菜单关闭时选择了一个选项,则认为该选项是用户想要的
		onMenuSelect(GuiService.SelectedObject)
	end
	
	-- 清除选择对象并播放关闭动画
	GuiService.SelectedObject = nil
	menuFrame:TweenSizeAndPosition(menuClosedSize, menuClosedPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quart, .4, true,
		function()
			-- 动画结束时的回调函数。如果用户没有重新打开菜单,则隐藏它
			if not menuOpen then
				menuFrame.Visible = false
			end
		end)
	menuOpen = false
end

鉴于游戏的剩余部分会依赖于这些设置,所以 closeMenu 函数首先会恢复 GuiNavigationEnabled 和 AutoSelectGuiEnabled。它还会解绑 onThumbstickMoved,便于操纵杆用于其他用途。然后,如果存在 SelectedObject,函数就会调用 onMenuSelect,作为参数传递给 SelectedObject。

onMenuSelect 表示你想要添加任意自定义代码的位置。根据传入的按钮,你可以在选定按钮的情况下,为需要生成的任何动作编写代码。

调用 onMenuSelect 后,closeMenu 会清除当前选项,并播放动画来隐藏菜单。

完成代码

下面是所有实施上述摇杆快速菜单的代码。代码应当包含在 LocalScript 内,且位于 StarterPlayerScriptsStarterGui 中。

创建摇杆快速菜单(完整代码) ```

local THUMBSTICK_DEADZONE = .4
local RADIUS = .5
local NUM_OPTIONS = 6
local ANGLE_OFFSET = 90
 
-- 服务
local ContextActionService = game:GetService("ContextActionService")
local GuiService = game:GetService("GuiService")
 
-- 菜单和背包使用的都是左操纵杆。禁用背包 UI 以避免冲突
game.StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false)
 
local player = game.Players.LocalPlayer
local menuItems = {}
 
-- 决定打开和关闭状态下菜单框架的位置和大小
local menuOpenPosition = UDim2.new(0.25, 0, 0.25, 0)
local menuOpenSize = UDim2.new(0.5, 0, 0.5, 0)
local menuClosedPosition = UDim2.new(0.5, 0, 0.5, 0)
local menuClosedSize = UDim2.new(0.005, 0, 0.005, 0)
 
-- 决定菜单选项的元素
local itemTemplate = Instance.new("TextButton")
itemTemplate.Size = UDim2.new(0.3, 0, 0.2, 0)
itemTemplate.AutoButtonColor = false
itemTemplate.TextScaled = true
 
-- 为菜单创建屏幕和框架
local menuScreen = Instance.new("ScreenGui", player:WaitForChild("PlayerGui"))
menuScreen.Name = "MenuScreen"
local menuFrame = Instance.new("Frame", menuScreen)
menuFrame.Size = menuClosedSize
menuFrame.Position = menuClosedPosition
menuFrame.Visible = false
menuFrame.BackgroundTransparency = 1
menuFrame.BorderSizePixel = 0
menuFrame.Name = "MenuFrame"
 
-- AutoSelectGuiEnabled 和 GuiNavigationEnabled 的存储
local wasAutoSelectGuiEnabled = GuiService.AutoSelectGuiEnabled
local wasGuiNavigationEnabled = GuiService.GuiNavigationEnabled
 
local function newMenuItem(name, angle, range)
	local newItem = {}
	local button = itemTemplate:Clone()
	button.Text = name
	button.Name = name
	local angleRadians = math.rad(ANGLE_OFFSET + angle)
	button.Position = UDim2.new(.5 + RADIUS * math.cos(angleRadians) - button.Size.X.Scale / 2, 0,
							 .5 - RADIUS * math.sin(angleRadians) - button.Size.Y.Scale / 2, 0)
	button.Parent = menuFrame
	newItem.Label = button
	newItem.Vector = Vector2.new(math.cos(angleRadians), math.sin(angleRadians))
	newItem.Range = range
	table.insert(menuItems, newItem)
end
 
for i = 1, NUM_OPTIONS do
	local angle = (360 / NUM_OPTIONS) * (i - 1)
	local name = "Option" .. i
	newMenuItem(name, angle, 360 / NUM_OPTIONS)
end
 
-- 通过菜单项目表,找到每个项目的向量和传递的向量之间的
-- 角度。如果角度小于项目范围的一半,则该项目
-- 被选中。
local function getButtonFromVector(vector)
	for i = 1, #menuItems do
		local item = menuItems[i]
		local dotProduct = vector.X * item.Vector.X + vector.Y * item.Vector.Y
		local angle = math.acos(dotProduct / vector.magnitude)
		if angle <= math.rad(item.Range) / 2 then
			return item.Button
		end
	end
	return nil
end
 
-- 绑定至左侧操纵杆的动作
local function onThumbstickMoved(actionName, inputState, inputObject)
	-- 确定操纵杆移动已经超过死区
	if inputObject.Position.magnitude >= THUMBSTICK_DEADZONE then
		-- 基于操纵杆位置计算角度
		local selectedButton = getButtonFromVector(inputObject.Position)
		GuiService.SelectedObject = selectedButton
	else
		-- 操纵杆移动在死区范围内,清除选择
		GuiService.SelectedObject = nil
	end
end
 
-- 物品被选择时要调用的函数。您应当在这里编写自定的代码
-- 以实现您预期的菜单
local function onMenuSelect(option)
	print(option, "selected")
end
 
local function openMenu()
	-- 存储 GuiNavigationEnabled 和 AutoSelectGuiEnabled 然后将二者均设置为 false
	wasGuiNavigationEnabled = GuiService.GuiNavigationEnabled		
	wasAutoSelectGuiEnabled = GuiService . AutoSelectGuiEnabled
	GuiService.GuiNavigationEnabled	= false
	GuiService.AutoSelectGuiEnabled = false
 
	-- 绑定 onThumbstickMoved 函数
	ContextActionService:BindAction("RadialMenu", onThumbstickMoved, false, Enum.KeyCode.Thumbstick1)		
 
	-- 确保框架可见并播放打开动画
	menuFrame.Visible = true
	menuFrame:TweenSizeAndPosition(menuOpenSize, menuOpenPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quart, .5, true)
	menuOpen = true	
end
 
local function closeMenu()
	-- 恢复 GuiNavigationEnabled 和 AutoSelectGuiEnabled
	GuiService.GuiNavigationEnabled	= wasGuiNavigationEnabled
	GuiService . AutoSelectGuiEnabled = wasAutoSelectGuiEnabled	
 
	-- 解绑 onThumbstickMoved 函数		
	ContextActionService:UnbindAction("RadialMenu")
 
	if GuiService.SelectedObject then
		-- 如果菜单关闭时已经选择了一个选项,则认为该选项为用户所想要的
		onMenuSelect(GuiService.SelectedObject)
	end
 
	-- 清楚选中的对象并播放关闭动画
	GuiService.SelectedObject = nil
	menuFrame:TweenSizeAndPosition(menuClosedSize, menuClosedPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quart, .4, true,
		function()
			-- 动画结束时的回调函数。如果用户未再打开菜单则隐藏它
			if not menuOpen then
				menuFrame.Visible =  false 
			end 
		end ) 
	menuOpen =  false 
end
 
local function toggleMenu(actionName, inputState)
	if inputState == Enum.UserInputState.Begin then
		openMenu()
	elseif inputState == Enum.UserInputState.End then
		closeMenu()
	end
end
 
ContextActionService:BindAction("ToggleMenu", toggleMenu, false, Enum.KeyCode.ButtonL1)


***__Roblox官方链接__:[创建摇杆快速菜单](https://developer.roblox.com/zh-cn/articles/Creating-a-Radial-Menu)