在Decentraland场景中添加UI

转发自官方公众号:DCL世界

在 9 月 16 日的 Game Jam 游戏开发活动开始前,我们邀请了六月份黑客马拉松的一些参与者在博客中公开他们的的场景设计开发秘密。本周的嘉宾是社区成员 surz, 也可以使用 @surz 在 Discord 或 Slack 上跟他联系。

我在 2018 年接触到了 Decentraland,并且对项目所采用的简洁、低多边形设计方案非常感兴趣。一年后,我参加了创作大赛 [https://decentraland.org/cn/blog/platform/creator-contest-highlights/],并投入了很多时间。场景编辑器是使用预制 3D 模型入门开发的绝佳工具。在创作大赛后,我收到了加入 6 月份黑客马拉松的邀请。凭借 Javascript、Python 和 C++(主要用于脚本)的一些基本知识以及我对 3D 建模的兴趣,我决定参加这次活动。

我创建了一个可视化的小型太阳系场景,用于教育和艺术展示。想法是创建一个可提供每个行星基本信息的天文馆类场景。除了这些信息之外,每个行星表面还会有简单的外貌景观以增强身临其境的感觉。通过这种方式,行星之间的不同以及它们与我们地球的差异使得场景变得更加真实和引人入胜。

您可以在此链接中查看场景。

下面是一些场景截图:

教程

很高兴分享我用 UI 做了哪些功能以及我如何使用 UI。本教程将重点介绍本项目 UI 的三个部分:有关太阳、水星和免责声明的菜单。

UI 中设计的图标和字符都归功于我的兄弟 Jevicho99。

您可以从此链接下载本教程的代码、2D 图像和 3D 模型。

如果您对 Decentraland 中的 UI 工作方式或代码示例有任何疑问,请查看 Decentraland 文档

好了,让我们开始吧!

准备 UI 素材

我们希望场景中在不同的时间使用几个不同的 UI 菜单。这些菜单中都有不可点击的静态部分和点击的动态部分。您可以在github页面的 /images 文件夹下找到本教程的图片。如果您愿意,请在阅读本教程时下载这些内容并继续学习。

以下是免责声明页面的静态不可点击部分:

以下是 Sun 和 Mercury UI 的静态,不可点击部分:

以下是 Sun 和 Mercury UI 的静态,不可点击部分:

关闭按钮:隐藏所有 UI 元素。此按钮将出现在所有屏幕上。

下一步按钮:在免责声明屏幕之间切换。

Facts:显示相关行星的介绍。再次单击时,它会切换到另一个介绍。

Compare:显示将当前行星与地球相比较的信息。

Artscape:显示行星表面的 3D 景观。

放在一起时,完整的 UI 布局应如下所示:

现在我们完成了屏幕设计,让我们在代码中实现它。

添加 canvas 组件和 container 容器

创建 UI 的第一步是添加一个画布 canvas 组件和一个矩形容器。此矩形将包含我们稍后添加的所有 UI 元素,并使其更容易定位。


// create canvas
const canvas = new UICanvas()
// create container inside canvas
const rect = new UIContainerRect(canvas)
rect.adaptHeight = true
rect.adaptWidth = true
rect.hAlign = 'left'
rect.vAlign = 'top'
rect.opacity = 0.8

请注意,矩形的不透明度设为 0.8,因此所有的 UI 都将部分透明。

添加静态内容

以下代码添加了 sun UI 的静态部分。


let sunUITexture = new Texture("images/UI/UI_sun.jpg")
const sunImgScreen = new UIImage(rect, sunUITexture)
sunImgScreen.hAlign = 'left'
sunImgScreen.vAlign = 'top'
sunImgScreen.sourceLeft = 0
sunImgScreen.sourceTop = 0
sunImgScreen.sourceWidth = 1024
sunImgScreen.sourceHeight = 483
sunImgScreen.width = 1024
sunImgScreen.height = 512

请注意,当我们创建 UIImage 组件时,第一个参数是 rect。这是我们为包含所有 UI 而创建的矩形。传递这个参数,使得图像成为该矩形的子图像并且其相对于矩形定位。

这里有很多属性,但大多数是一目了然的。您可能会想知道 sourceLeftsourceTopsourceWidth 和 sourceHeight 是用来做什么的。这些是用于图像的裁剪显示。因为我们在这里使用了完整的图片,所以将它们设为全尺寸。

如果您坚持到这里了,那么现在是使用 dcl start 尝试场景的好时机。现在应该能在 UI 上看到此图像。

由于我们不希望此 UI 始终显示,因此我们将再添加一行将 visible 属性设置为 false。使图像不可见一直到 visible 值发生变化,我们将在稍后介绍。


sunImgScreen.visible = false

为在场景中添加其余的消息窗口,我们使用了几乎相同的代码,除了使用不同的图像作为纹理。

添加关闭按钮

添加按钮与添加图像没有太大差别,按钮是具有额外功能的图像。例如,此代码向屏幕添加 close 按钮。


let imgCloseBtn = new Texture("images/UI/close.png")
const closeBtn = new UIImage(rect, imgCloseBtn)
closeBtn.name = 'close_btn'
closeBtn.width = '50px'
closeBtn.height = '50px'
closeBtn.sourceWidth = 112
closeBtn.sourceHeight = 112
closeBtn.positionX = 953
closeBtn.positionY = -5
closeBtn.isPointerBlocker = true
closeBtn.onClick = new OnClick(() => {        
   log("Close Button Clicked")        
   canvas.visible = false        
}) 

您会注意到代码与我们之前添加的静态图像没有太大区别。唯一重要的区别是:

  • isPointerBlocker 属性设置为 true,使得组件可单击,而不是单击可能显示在组件后面的内容。
  • onClick 属性,用于确定单击时要执行的内容。在这里,它将整个 UI 的可见性设置为 false

对于 UI 中的其他按钮,我们都使用了与上面相同的代码,只是 onClick 和定位不一样。

添加 UI 文本

与我们作为图像的一部分导入的固定文本不同,我们希望 UI 可以动态更改文本。为此,我们创建一个 UIText 组件。以下代码添加了一个显示 “WELCOME” 的文本组件。我们稍后会让场景更改此文本,以便显示当前行星的相关不同信息。


const factTxt = new UIText(rect)
factTxt.outlineColor = new Color4(0.7, 1, 0.8, 1)
factTxt.value = 'WELCOME'
factTxt.fontSize = 22
factTxt.width = 500
factTxt.height = 205
factTxt.positionX = 455
factTxt.positionY = 0
factTxt.color = new Color4(0.7, 1, 0.8, 1)
factTxt.textWrapping = true

UI 对象分组

到目前为止,对于许多需要一个简单的用户界面,使用中也会更改太多的用例来说可能已经够用。但是,如果您想要处理多个 UI 屏幕,每个屏幕都有各种组件,并且可以轻松地从一个屏幕切换到另一个屏幕时怎么办?以下是我提出的可以轻松扩展的解决方案。

接下来的示例,我们改为创建每屏所需的所有 UIImage 组件,这些组件全部以 JSON 格式列出,并根据功能进行分组。然后在屏幕切换时调用它们。


const staticScreenGroup = {
   "sun": sunImgScreen,
   "mercury": merImgScreen,
   "disclaimer1": discImgScreen1,
   "disclaimer2": discImgScreen2
}
const closeMenuGroup = {
   "closeBtn": closeBtn
}
const disclaimerMenuGroup = {
   "nextBtn": nextBtn
}
const planetMenuGroup = {
   "factBtn": factBtn,
   "compareBtn" : compareBtn,
   "artscapeBtn" : artscapeBtn,
   "factTxt" : factTxt
}

处理分组的 UI 对象

为了使用我们刚创建的组件列表,我们还将编写一个函数来解析列表并根据屏幕需要使元素显示。

此函数有三个布尔值参数,每个参数用以打开或关闭 UI 的不同部分:planetMenu(显示 fact,compare 和 artscape 按钮,以及 fact 文本),disMenu(显示免责声明屏幕中的下一个按钮)和 closeMenu(显示关闭按钮)


function stateDynamicUI(bPlanetMenu: boolean, bDisclaimerMenu: boolean, bCloseMenu: boolean) {

   for (let key in planetMenuGroup) {
      planetMenuGroup[key].visible = bPlanetMenu
   }
   for (let key in disclaimerMenuGroup) {
      disclaimerMenuGroup[key].visible = bDisclaimerMenu
   }
   for (let key in closeMenuGroup) {
      closeMenuGroup[key].visible = bCloseMenu
   }
}

还缺少选择我们想要显示的基本静态图像功能。我们将创建包含一个函数的单例对象,该函数使所有静态 UI 元素不可见,除了我们当前要显示的元素。


const stateInfoUI = (
   function () {
     let UI_show: UIImage
     return {
        changeCurrentUI: function(ui_screen) {
           if (UI_show) {
              UI_show.visible = false
           }
           UI_show = ui_screen
           UI_show.visible = true
           canvas.visible = true
        },
        getCurrentUI: function () {
           return UI_show
        }
     }
}())

从虚拟世界的对象中打开 UI

现在将三个 3D 方框做成的简单的 3D 菜单添加到我们的场景中。单击各个方框时,会打开不同的 UI,包括所有相关元素。期望的效果类似于下面的 GIF。

粘贴下面的代码以添加 3D 实体:


// parent entity
const menu3D = new Entity()
engine.addEntity(menu3D)

const sunMenu = new Entity()
sunMenu.addComponent(new BoxShape())
sunMenu.addComponent(new Transform({
   position: new Vector3(8, 1.5, 8),
   scale: new Vector3(0.1, 0.25, 0.5)
}))
sunMenu.setParent(menu3D)
engine.addEntity(sunMenu)

const mercuryMenu = new Entity()
mercuryMenu.addComponent(new BoxShape())
mercuryMenu.addComponent(new Transform({
   position: new Vector3(8, 1, 8),
   scale: new Vector3(0.1, 0.25, 0.5)
}))
mercuryMenu.setParent(menu3D)
engine.addEntity(mercuryMenu)

const disclaimerMenu = new Entity()
disclaimerMenu.addComponent(new BoxShape())
disclaimerMenu.addComponent(new Transform({
   position: new Vector3(8, 0.5, 8),
   scale: new Vector3(0.1, 0.25, 0.5)
}))
disclaimerMenu.setParent(menu3D)
engine.addEntity(disclaimerMenu)

现在我们可以在 3D 实体中添加一个 OnClick 组件,并调用我们在上一步中创建的显示 UI 元素的函数。


menu_sun.addComponent(
   new OnClick(e => {
      log("sun CLICKED")
      planetMenuGroup.factTxt.value = "WELCOME"
      stateInfoUI.changeCurrentUI(staticScreenGroup.sun)
      stateDynamicUI(true, false, true)
}))

上面的代码适用于 ‘sun’ 菜单,对于其他菜单,你可以以相同的代码不同的参数调用 stateInfoUI.changeCurrentUI() 和 stateDynamicUI() 函数。免责声明菜单将使用 (true, true, false) 参数调用 stateDynamicUI 函数,以显示“关闭”和“下一步”按钮,但不显示其他按钮。

现在,当您单击 3D 对象时,就可以在屏幕上打开相应的 UI 菜单。

在“下一步”按钮中添加功能

现在我们可以轻松地在每个屏幕之间切换所有元素,让我们在 UI 屏幕上为更多按钮添加功能。我们已经演示了如何通过设置整个 UI 的可见性来为“关闭”按钮提供功能。为了简化教程,我们只示范 Next 和 Facts 按钮。

下面是 ‘next’ 按钮的 OnClick 功能,它使用我们创建的功能在两个免责声明屏幕之间切换:


nextBtn.onClick = new OnClick(() => {
   log("Next Button Clicked")
   log(stateInfoUI.getCurrentUI().name)
   if (stateInfoUI.getCurrentUI().name == 'disclaimer1_screen') {
      stateInfoUI.changeCurrentUI(staticScreenGroup.disclaimer2)
   }
   else {
      stateInfoUI.changeCurrentUI(staticScreenGroup.disclaimer1)
   }
})

为 Facts 按钮添加功能

现在我们将向 “Facts” 按钮添加功能,以便显示有关当前行星的介绍:

首先,我们定义一个字符串数组,列出我们想要显示的内容。


const sun_facts: string[] = [
   "Sun's gravity holds the solar system together, keeping everything from the biggest planets to the smallest particles of debris in its orbit",
   "At the equator, the Sun spins once about every 25 days, but at its poles the Sun rotates once on its axis every 35 Earth days",
   "By mass, the Sun is about 70.6 % Hydrogen and 27.4 % Helium",
   "Sun releases a constant stream of particles and magnetic fields called the solar wind that can slams worlds across the solar system with particles and radiation"
]

然后我们将创建一个单例来处理 UI 文本的切换。此对象有一个在预定义字符串中循环并返回其中一个的函数。


const FactsModule = (function () {
    let facts_sun_ind = 0, facts_mer_ind = 0
    let fact_arr: string[]
    let fact_ind: number
    return {
        setFact: function (ui_screen) {
            if (ui_screen.name == "sun_screen") {
                log("fact for SUN")
                fact_arr = sun_facts
                facts_sun_ind = (facts_sun_ind + 1) % sun_facts.length
                fact_ind = facts_sun_ind
                return fact_arr[fact_ind]
            }
        }
    }
}())

最后一步是在 ‘Facts’ 按钮的 OnClick 属性中调用此函数,并将 UI 中的文本组件的值更改为此函数的返回值。


factBtn.onClick = new OnClick(() => {
   log("Fact Button Clicked")
   let factStr = FactsModule.setFact(stateInfoUI.getCurrentUI())
   planetMenuGroup.factTxt.value = factStr
})

有关场景的最终完整代码,请查看此项目的GitHub 库

结束语

Decentraland 提供了一个平台来创建一个由区块链管理的虚拟世界,我认为这既有趣而又具有挑战性。随着平台的发展,我愿意探索平台内部可能的机制和用例来创建更多高质量的内容。

如果您对如何提高上述代码的效率有任何想法,可以加入 Discord #sdk 频道参与讨论,我很想听听你的反馈意见!

当然,如果您对开发场景内容感兴趣,请参与 9 月 16 日开始的 GameJam 游戏开发活动。马上加入,我们虚拟世界见!

分享你的喜悦!