Google Blockly Reimplementation with Unity/C#(4)

8min read

Contents

  1. Introduction
  2. Blockly Model
  3. Code Generator, Interpreter and Runner
  4. UGUI Design

For English:

  1. Introduction
  2. Blockly Model
  3. Code Interpreter and Runner
  4. UGUI Design


UGUI Design

在设计Blockly UI时,主要考虑解决以下几个问题:

  1. 自动生成Block View,可以在Editor里预生成Prefab,也可以Runtime时动态生成GameObject。
  2. 动态Layout,根据Block View的实际计算大小,以及View之间的相互连接,实现动态布局、缩放。
  3. 动态Layout后,Block View底图的实时绘制。
  4. 独立Blockly Model模块,采用观察者模式监听Model的变化。
  5. Block View最近连接的搜索。
  6. 可重建Workspace,可复制Block View,可变形Block View。

Hierarchy of Views

首先,需要设计一套View Hierarchy,既能符合Block Model的结构,表现Blocks之间的Connection,又能结合UGUI Transform Hierarchy,实现动态Layout计算。

回顾讲解Block Model的章节中关于Block Hierarchy的介绍,可知Block包括Connections、Inputs,而Inputs包括Fields、Connections,Connections可以连接其他Blocks,这些元素均需在UI上体现出来。在此基础上,我们还增加了一个LineGroup,因为某些Block View需要将Inputs分布在多行,LineGroup是用来包裹一行的Inputs。因此,最终的View Hierarchy如下:

hierarchy of view:

- Block
  - ConnectionOutput
  - ConnectionPrev  
  - ConnectionNext
    - Block(Next)

  - LineGroup
    - Input
      - Field 
      - Field 
      ...
      - ConnectionInput
        - Block(Input)
    - Input
      ...
  - LineGroup
    ...
  ...
- Block
  ... 

抽象基类BaseView

Hierarchy中的每一个元素,都是一个View,因此我们抽象了基类BaseView,它继承MonoBehaviour,管理了:

  • 链式结构:Parent, Childs, Previous, Next。
  • 自下而上的迭代式Layout Update,详见这里

子类Views类型

依据Hierarchy,设计了6个基本的View类型:

BlockView, ConnectionView, LineGroupView, InputView,FieldView, ConnectionInputView

其中ConnectionInputView继承自ConnectionView,在Model中都体现为Connection,但在UI表现上ConnectionInputView是包裹输入Block的,而ConnectionView是挂载Next Block。

基于这样的设计,可以很好的解决问题123

Auto Build Block View

无论是Editor预生成Prefab,还是Runtime动态生成,Block View是依赖于Block Model来生成的。依照自上而下的顺序依次创建:

Block -> Connection, LineGroup -> Input -> Field

并且同时设置好链式关系,通过MonoBehaviour序列化保存下来。

Dynamic Layout

为什么需要Dynamic Layout,它需要做什么?先看下面两个例子:

例1

->

例2

->

可以看出:

  1. Block的Size会根据其自身Fields大小,以及其Child Blocks大小进行缩放;
  2. Block自身Fields的起始位置,以及Blocks相互之间的起始位置,都会根据缩放后的大小进行重新摆放;

因此经过Dynamic Layout之后,布局更紧凑,更美观!那么如何实现的?

UGUI有一套Layout机制,是依赖于Transform Hierarchy,在每一个生命周期的Update之后统一计算的,先后不可控,因此无法根据View的依赖关系按照正确的顺序计算。

什么是正确的顺序?四个字概括:自下而上。请看流程图:

代码大致如下:

public void UpdateLayout(Vector2 startPos)
{
    XY = startPos;
    Size = CalculateSize();

    switch (Type)
    {
        case ViewType.Field:
        case ViewType.Input:
        case ViewType.ConnectionInput:
        case ViewType.LineGroup:
        {
            if (m_Next == null /*|| (!changePos && !changeSize)*/)
            {
                //reach the last child, or no change in current hierarchy, update it's parent view
                m_Parent.UpdateLayout(m_Parent.SiblingIndex == 0 ? m_Parent.HeaderXY : m_Parent.XY);
            }
            else
            {
                //update next
                if (Type != ViewType.LineGroup)
                {
                    // same line
                    startPos.x += Size.x + BlockViewSettings.Get().ContentSpace.x;
                }
                else
                {
                    // start a new line
                    startPos.y -= Size.y + BlockViewSettings.Get().ContentSpace.y;
                }

                BaseView topmostChild = m_Next.GetTopmostChild();
                if (topmostChild != m_Next)
                {
                    //need to update from its topmost child
                    m_Next.XY = startPos;
                    topmostChild.UpdateLayout(topmostChild.HeaderXY);
                }
                else
                {
                    m_Next.UpdateLayout(startPos);
                }
            }
            break;
        }
        case ViewType.Connection:
        case ViewType.Block:
        {
            //no need to update its m_Next, as it is handled by Unity's Transform autolayout 
            //update its parent directly
            if (m_Parent != null)
            {
                m_Parent.UpdateLayout(m_Parent.SiblingIndex == 0 ? m_Parent.HeaderXY : m_Parent.XY);
            }
            break;
        }
    }
}

Adjusted Background

动态Layout之后,带来的就是底图的实时绘制,当然采用了九宫格的方式,但是简单的九宫格缩放不能满足需求,看这个:

而这里只用了一张原图:

当然颜色是自定义设置的,通过UGUI Image面板设置。

其实方法很简单,参照UGUI中绘制Image的方法,重载OnPopulateMesh(VertexHelper)方法,按照九宫格的方式设置好顶点、uv,即可:

->

上图用圆点标记的,是由外部Layout计算好之后的每一个LineGroup的顶点min, max。分析与代码详见这篇

动态绘制底图还有一个好处是:不需要拼接图片,减少了资源量,并且避免了Draw Call的增加。

Observer Pattern

因为一开始设计的初衷是Model模块完全独立于另外两个模块Interpreter、UI,如果想要移植,完全可以以Model为核心,重新设计这两个模块。因此需要实现Model模块的完全解耦,而Google Blockly Web版是将UI与Model耦合在一起了,也许并不需要考虑移植。

观察者模式,是实现UI与Model之间通信的最好方式,Model是事件的发布者,是任何变化、计算的核心,而UI是监听者,监听Model的变化更新表现,以及将用户输入转化为通知Model变化的信号。

这是个经典的设计模式,在此不再赘述。

Binary Search Nearest

搜索最近连接,如果全局遍历所有的Connection Point,时间复杂度为O(n),并且需要计算距离进行比对,无疑是一项耗cpu的操作。所幸的是Google Blockly提供了一套算法方案,二分搜索法。

二分搜索法的前提是,有序序列,因此需要对Workspace中的所有Connection Point进行排列。做法是:

  1. 根据connection point的y坐标进行排序。
  2. 每当Block改变时(增、删、移动),将其Connection Point插入到Map中合适的位置,这个位置也是通过二分搜索法查找,只考虑y坐标。
  3. 当要搜索最近Connection时,先通过y坐标找到其在Map中的位置,然后向两边通过比对距离来查找,也考虑Connection的兼容性(例如:数学运算符两边只允许数字输入)。

时间复杂度为O(logn),并且也平均减少了计算量。

Manipulate Views

基于以上,操作Block View就变得很方便,因为自动化,动态,并且极大程度的优化了性能。

重建Workspace

Model层可以将Workspace保存为Xml文件,Xml文件可以再重建Workspace(见前文)。通过Workspace中Block Models,可以动态创建Block Views,并依据Connections,以及顶层Blocks的位置,实现自动Layout。

复制Block View

Workspace可以保存为Xml文件,当然是基于Block可以保存为一个Xml Node,因此复制Block可以通过将原Block保存为Xml Node,然后从Xml Node重建一个新的Block,再通过Block动态创建Block View。

变形Block View

Block具有Mutation特性,可以动态修改Block结构,因此动态生成Block View的功能为此提供了便利,可以动态增删Input Views。


这整套UI方案来自于HTML流体设计的灵感。技术是相通的😄.


MORE FROM THE BLOG

The UGUI Design of uBlockly...

For Chinese:...

5min read

The Interpreter and Runner of...

For Chinese:...

6min read

The Blockly Model of uBlockly...

For Chinese:...

4min read

Introduction of uBlockly - Reimplementation...

For Chinese:...

1min read