文本与聊天过滤

Time:20 分钟

保证玩家与游戏环境安全的重要方法之一就是使用恰当的文本过滤功能。Roblox 内置的文本过滤功能不仅可以防止用户看到污言秽语等不当消息,还可以屏蔽相关个人信息,防止身份泄露。由于 Roblox 拥有大量年轻用户群,防止其分享或阅读不当内容是极有必要的。

由于关键词过滤对于营造安全环境极为重要,Roblox 会对游戏内容进行主动监控,确保其符合特定标准。如果游戏被举报或自动检测到未应用过滤功能,我们将会暂时对其进行关闭,直至开发人员采取适当措施应用过滤功能。

如何过滤文本

通过 TextServiceTextService/FilterStringAsync|FilterStringAsync() 函数可以对文本进行过滤。此函数会提取文本字符串(作为输入)及文本创建玩家的 ID,然后返回可用于发布过滤后字符串的 TextFilterResult 对象。

local TextService = game:GetService("TextService")
local filteredTextResult = TextService:FilterStringAsync(text, fromPlayerId)

TextService/FilterStringAsync|FilterStringAsync() 应在服务器上调用。在客户端上对其调用时会失败。

此函数可用于过滤聊天和非聊天情景中特定用户或全部用户的字符串。此函数所返回的对象 TextFilterResult 有三种调用方法:TextFilterResult/GetChatForUserAsync|GetChatForUserAsync()TextFilterResult/GetNonChatStringForBroadcastAsync|GetNonChatStringForBroadcastAsync()TextFilterResult/GetNonChatStringForUserAsync|GetNonChatStringForUserAsync()

local TextService = game:GetService("TextService")
local filteredText = ""
local success, errorMessage = pcall(function()
	filteredTextResult = TextService:FilterStringAsync(text, fromPlayerId)
end)
if not success then
	warn("Error filtering text:", text, ":", errorMessage)
	-- 在此处放置代码以处理过滤失败
end

请注意,TextService/FilterStringAsync|FilterStringAsync() 和所有 TextFilterResult 函数都会进行内部 web 调用,因此偶尔会发生调用失败的情况。所以,上述函数应当始终被封装在 pcall 中。如果包含过滤器函数的 pcall 调用失败,请切勿按预期继续使用此文本(即使发布空文本字段也不要发布未过滤文本)。

示例 1

此示例设置了一个小组件,可让玩家向其他玩家发送消息。此类小组件需要至少两个脚本:一个用于处理输入信息和显示消息的 LocalScript 和一个用于过滤服务器上消息的 Script 。由于示例模拟了一名玩家向另一名特定玩家发送消息的情景,应使用函数 TextFilterResult/GetChatForUserAsync|GetChatForUserAsync()。可沿用此实用示例

LocalScript

-- LocalScript

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local screen = playerGui:WaitForChild("MessageScreen")
local sendMessageEvent = ReplicatedStorage:WaitForChild("SendPrivateMessage")

-- sendFrame(发送框)的 GUI 元素
local sendFrame = screen:WaitForChild("SendFrame")
local recipientField = sendFrame:WaitForChild("Recipient")
local writeMessageField = sendFrame:WaitForChild("Message")
local sendButton = sendFrame:WaitForChild("Send")

-- receiveFrame(接收框)的 GUI 元素
local receiveFrame = screen:WaitForChild("ReceiveFrame")
local senderField = receiveFrame:WaitForChild("From")
local readMessageField = receiveFrame:WaitForChild("Message")

-- 点击发送按钮时将会调用
local function onSendClicked()
	-- 尝试寻找接收消息的玩家,如果该玩家存在才会发送消息
	local recipient = Players:FindFirstChild(recipientField.Text)
	local message = writeMessageField.Text
	if recipient and message ~= "" then
		-- 发送消息
		sendMessageEvent:FireServer(recipient, message)
		-- 清理 send frame(发送框)
		recipientField.Text = ""
		writeMessageField.Text = ""
	end 
end

-- 当发送信息事件触发时被调用,也就是说此客户端收到了消息
local function onReceiveMessage(sender, message)
	-- 将发送者和消息本身填写至接收框的字段中
	senderField.Text = sender.Name
	readMessageField.Text = message
end

-- 绑定事件函数
sendButton.MouseButton1Click:Connect(onSendClicked)
sendMessageEvent.OnClientEvent:Connect(onReceiveMessage)

Script

-- 脚本

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TextService = game:GetService("TextService")

local sendMessageEvent = ReplicatedStorage.SendPrivateMessage

local function getTextObject(message, fromPlayerId)
	local textObject
	local success, errorMessage = pcall(function()
		textObject = TextService:FilterStringAsync(message, fromPlayerId)
	end)
	if success then
		return textObject
	end
	return false
end

local function getFilteredMessage(textObject, toPlayerId)
	local filteredMessage
	local success, errorMessage = pcall(function()
		filteredMessage = textObject:GetChatForUserAsync(toPlayerId)
	end)
	if success then
		return filteredMessage
	end
	return false
end

-- 当客户端发送消息时进行调用
local function onSendMessage(sender, recipient, message)
	if message ~= "" then
		-- 对输入的消息进行过滤并发送过滤后的消息
		local messageObject = getTextObject(message, sender.UserId)

		if messageObject then
			local filteredMessage = getFilteredMessage(messageObject, recipient.UserId)
			sendMessageEvent:FireClient(recipient, sender, filteredMessage)
		end
	end
end

sendMessageEvent.OnServerEvent:Connect(onSendMessage)

示例 2

本示例设置了一个对话框,可让玩家在标牌上撰写消息。由于服务器中的所有人(包括在撰写消息玩家离开后加入游戏的玩家)都可查阅此消息,因此必须使用 TextFilterResult/GetNonChatStringForBroadcastAsync|GetNonChatStringForBroadcastAsync() 对消息文本进行过滤。可沿用此实用示例

LocalScript

-- LocalScript
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local screen = playerGui:WaitForChild("MessageScreen")

-- 对话框的 GUI 元素
local frame = screen:WaitForChild("Frame")
local messageInput = frame:WaitForChild("Message")
local sendButton = frame:WaitForChild("Send")

-- RemoteEvent,用于向服务器发送需要过滤与显示的文本 
local setSignText = ReplicatedStorage:WaitForChild("SetSignText")

-- 按下按钮后进行调用
local function onClick()
	local message = messageInput.Text
	if message ~= "" then
		setSignText:FireServer(message)
		frame.Visible = false
	end
end

sendButton.MouseButton1Click:Connect(onClick)

Script

-- 脚本

local TextService = game:GetService("TextService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local sign = game.Workspace.Sign
local signTop = sign.Top
local signSurfaceGui = signTop.SurfaceGui
local signLabel = signSurfaceGui.SignLabel

local setSignText = ReplicatedStorage.SetSignText

local function getTextObject(message, fromPlayerId)
	local textObject
	local success, errorMessage = pcall(function()
		textObject = TextService:FilterStringAsync(message, fromPlayerId)
	end)
	if success then
		return textObject
	elseif errorMessage then
		print("生成 TextFilterResult 时出现错误", errorMessage)
	end
	return false
end

local function getFilteredMessage(textObject)
	local filteredMessage
	local success, errorMessage = pcall(function()
		filteredMessage = textObject:GetNonChatStringForBroadcastAsync()
	end)
	if success then
		return filteredMessage
	elseif errorMessage then
		print("过滤消息时出现错误:", errorMessage)
	end
	return false
end

-- 当客户端发送撰写标牌的请求时触发
local function onSetSignText(player, text)
	if text ~= "" then
		-- 过滤输入消息并发送过滤后的消息
		local messageObject = getTextObject(text, player.UserId)
		local filteredText = ""
		filteredText = getFilteredMessage(messageObject)
		signLabel.Text = filteredText
	end
end

setSignText.OnServerEvent:Connect(onSetSignText)

需要过滤的文本

任何开发人员无法专门控制且将会在屏幕上显示的文本都应当被过滤。这通常涵盖了玩家所控制的各种文本,但如果想要确保游戏符合 Roblox 的过滤规则,请务必考虑下述其它几种情况:

玩家输入信息

无论文本的输入与显示方法,任何玩家撰写的拟显示文本都必须经过过滤。最常见的输入文本方式是通过 TextBox,但获取玩家文本输入的方式并不仅限于此,例如带有角色按钮的自定义 GUI 和 3D 空间中的交互式键盘模型等。

AlternateInput0.png

AlternateInput1.png

除了非传统的新颖输入方法之外,还有许多显示文本的方法。例如,单词可以用 3D 部件拼写,Humanoid|Humanoid (人形对象)的 Model|Model (模型)可以显示其名称。如果此类显示内容可以被除作者外的其他玩家看到,则该文本在过滤后方可显示。

AlternateDisplay0.png

AlternateDisplay1.png

随机字词

某些游戏可能会用到以随机字符生成字词后显示给玩家的机制,但也有可能在过程中生成不当用语。在此类情况下,随机文字的显示结果应通过服务器中的过滤器发送,并可在 TextService/FilterStringAsync|FilterStringAsync() 中使用将查看这些字词的玩家用户 ID。

举例来说,下列代码会在玩家加入游戏时向其发送一个稍后会进行显示的随机字词。代码将在循环中生成随机字词,直至发现没有被过滤器更改的字词为止。可沿用此实用示例

local TextService = game:GetService("TextService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local sendRandomWordEvent = ReplicatedStorage.RandomWordEvent
local ALPHABET= "abcdefghijklmnopqrstuvwxyz"
local MIN_LENGTH = 3
local MAX_LENGTH = 8
-- 生成随机词的函数
local function generateRandomWord()
	local length = math.random(MIN_LENGTH, MAX_LENGTH)
	local text = ""
	for index = 1, length do
		local randomLetterIndex = math.random(1, string.len(alphabet))
		text = text .. string.sub(ALPHABET, randomLetterIndex, randomLetterIndex)
	end
	return text
end

local function getTextObject(message, fromPlayerId)
	local textObject
	local success, errorMessage = pcall(function()
		textObject = TextService:FilterStringAsync(message, fromPlayerId)
	end)
	if success then
		return textObject
	elseif errorMessage then
		print("生成文本对象时出现错误")
	end
	return false
end

local function getFilteredMessage(textObject, toPlayerId)
	local filteredMessage
	local success, errorMessage = pcall(function()
		filteredMessage = textObject:GetNonChatStringForUserAsync(toPlayerId)
	end)
	if success then
		return filteredMessage
	elseif errorMessage then
		print("过滤消息时出现错误:", errorMessage)
	end
	return false
end

-- 当玩家加入游戏时进行调用
local function onPlayerJoined(player)
	local text = ""
	local filteredText = ""
	-- 生成随机词,直到生成的词能够通过过滤为止
	repeat
		filteredText = ""
		text = generateRandomWord()
		-- 过滤输出消息并发送过滤后的消息
		local messageObject = getTextObject(text, player.UserId)
		filteredText = getFilteredMessage(messageObject, player.UserId)
	until text == filteredText
	if text == filteredText then
		print("消息为", text, "过滤消息为", filteredText)
	end
	-- 将生成的随机词发送至客户端
	sendRandomWordEvent:FireClient(player, text)
end
game.Players.PlayerAdded:Connect(onPlayerJoined)

外部源

一些游戏会与外部 Web 服务器相连。在某些情况下,这种连接将用于获取游戏中显示的信息内容。如果外部站点的内容不在开发人员完全控制之下,且第三方可能会对站点内容进行修改,则在显示此内容前应对其进行过滤。

local TextService = game:GetService("TextService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")

local sendRandomName = ReplicatedStorage.SendRandomName
local randomNameWebsiteAddress = "http://www.roblox.com/randomname"
local nameTable = nil

local function initializeNameTable()
	local nameTableJSON = nil
	local success, message = pcall(function()
		nameTableJSON = HttpService:GetAsync(randomNameWebsiteAddress)
	end)
	if success then
		nameTable = HttpService:JSONDecode(nameTableJSON)
		print("nameTable 为:", nameTable)
	end
end

local function onPlayerJoin(player)
	if nameTable then
		local randomName = ""
		local filteredName = ""
		local filteredNameObject
		repeat
			randomName = nameTable[math.random(#nameTable)]
			local success, errorMessage = pcall(function()
				filteredNameObject = TextService:FilterStringAsync(randomName, player.UserId)
			end)
			if success then
				print("过滤对象创建成功")
			elseif errorMessage then
				print("创建过滤对象时发生错误")
			end
			local success, errorMessage = pcall(function()
				filteredName = filteredNameObject.GetNonChatStringForUserAsync(player.UserId)
			end)
			if success then
				print("过滤名称创建成功")
			elseif errorMessage then
				print("创建过滤名称时发生错误")
			end
		until randomName == filteredName
		sendRandomName:FireClient(sendRandomName)
	end
end

initializeNameTable()
game.Players.PlayerAdded:Connect(onPlayerJoin)

已储存文本

很多游戏都会使用articles/Data store|数据储存来存储文本。例如,游戏可能会存储聊天日志或玩家昵称等。在这些情况下,如果需要过滤储存内的文本,建议在检索文本时对其进行过滤。可籍此确保文本将会使用最新版本的过滤器。

local TextService = game:GetService("TextService")
local DataStoreService = game:GetService("DataStoreService")

local petData = nil
local petCreator = require(game.ServerStorage.PetCreator)

local function onPlayerJoin(player)
	local data = {}
	local success, message = pcall(function()
		data = petData:GetAsync(player.UserId)
	end)
	if success then
		local petName = data.Name
		local petType = data.PetType
		local filteredName = ""
		local filteredNameObject
		local success, message = pcall(function()
			filteredNameObject = TextService:FilterStringAsync(petName, player.UserId)
		end)
		if success then
			local worked, errorMessage = pcall(function()
				filteredName = filteredNameObject:GetNonChatStringForBroadcastAsync()
			end)
			if worked then
				petCreator:MakePet(player, petType, filteredName)
			end
		end
	end
end

local success, message = pcall(function()
	petData = DataStoreService:GetDataStore("PetData")
end)
if success then
	game.Players.PlayerAdded:Connect(onPlayerJoin)
end

例外情况

文本过滤中的一个例外情况是在向玩家显示他们自己编写的文本时无需对其进行过滤,但对此类情况仍然需要加以考虑。

使用聊天过滤器功能过滤文本需耗费少量时间。举例来说,假设玩家输入了想要显示的消息。该文本必须发送到服务器,经过滤后发送回客户端,其中每个阶段都需要一定量的时间。当使用此过程时,输入消息与返回过滤消息之间可能会出现明显延迟。

FilterPath0.png

向其他玩家发送消息时,这一流程是必要的(由于其他玩家所看到的文本需要经过过滤)。但撰写消息的玩家应可在聊天记录中即刻看到自己的消息。为了便于聊天进行,Roblox 对此特殊情况内置了特别的处理办法:如果玩家仅使用 TextBox 输入文本,则生成的文本不必针对该玩家进行过滤,且将会在此玩家屏幕上立即显示。

FilterPath1.png

重要提醒:此除外情况在检索已存储的消息时不适用。Roblox 用于检测过滤是否正确进行的自动检查会忽略键入 TextBox 中的文本,但仅限于使用 TextBox 的同一会话中。如果玩家的文本已被保存,之后在玩家重新加入游戏时被进行检索,则需要在过滤后才会进行显示(其对象包括撰写该文本的玩家)。

Roblox官方链接:文本与聊天过滤