基础知识

本文介绍 Mini Studio 能实现的一些基础功能。

读者需要对 Mini Studio 的有一个基础的了解,并能简单进行操作。

官方网站资料:
https://studio.mini1.cn/wiki/Tutorials/Introduce/Introduce.html

studio界面基础操作:
https://studio.mini1.cn/wiki/Tutorials/Introduce/BaseOperation/StudioMenu.html

关于项目建立和搭建的基础操作,请参考另外一个demo RPG游戏的文档:
RPG游戏demo设计文档

准备工作

打开studio登陆后,新建一个空地图(Empty Map)项目,工程名自定义即可。

打开主界面后,首先在WorkSpace中放置一个大方块g0,作为地图的地基。
再分别放置方块g0_copy, g0_copy_2, g0_copy_3, g0_copy_4作为围墙:
所有的脚本范例,都将在这个范围内运行。



文档中一共包含了16个范例:

脚本范例分成客户端和服务器两类,为什么要这样来区分呢? 1 客户端以表现层为主: GUI界面,镜头控制,射线,声音等功能是客户端独有的 2 服务端以数据层为主: 运行在远程服务器,可以存储数据,防止很多作弊 3 一些特效只在客户端处理,性能更好 客户端侧范例 case3 -- 场景中增加一个NPC模型,播放动作,行走,需要读取animation,只能在客户端执行 case4 -- 测试GUI界面 建立一个按钮,按钮点击后,打开一个选择框 有界面操作,只能在客户端运行 case6 -- 玩家走近NPC后,NPC定住,弹出GUI界面 case7 -- 根据GUI按钮NPC选择不同逻辑 case10 -- 给玩家增加一个攻击按钮,点击的时候会向前方攻击 case14 -- 鼠标点击选中物体判断 pickup case15 -- 检查射线ray碰撞物体 case16 -- 播放声音特效 服务器侧范例 case1 -- 场景中增加方块,修改大小,位置,材质 case2 -- 设置一个触发器方块,碰撞后进行变色,位置修改 case5 -- 新建一个NPC,使用Act组件,NPC随机行走 case8 -- 生成一个建筑物,增加一个门,碰撞打开,自动关闭 case9 -- NPC判断与玩家的距离,靠近或者攻击 case11 -- 序列化一个table数据,并存在云端 case12 -- 序列化一个table数据,并存在云端(异步模式) case13 -- 建立一个立方体区域,判断物体进入
-- 在场景中增加一个方块,修改大小,位置,材质

function  MTestcase.case1()
    print( "MTestcase.case1" );

    -- 增加一个cube立方体
    local obj = SandboxNode.new('GeoSolid', game.WorkSpace )
    obj.Name = 'cube_test1'
    obj.GeoSolidShape = Enum.GeoSolidShape.Cuboid   --Cuboid立方体   Cylinder圆柱体

    obj.LocalPosition = Vector3.new( 100, 300, 100 );     --位置
    obj.LocalScale    = Vector3.new( 1, 2, 3 );           --缩放
    obj.Euler         = Vector3.new( 0, 45, 0 );          --45度

    log( 'case1 ', obj.LocalEuler,  obj.Euler )

    if  true then
        --修改材质方法1 直接设置图片资源id
        -- (需要先在 资源背包 - 云资源中上传一个图片,并右键获得资源id )
        obj.TextureId = 'RainbowId&filetype=5://229435794039771136'
                       --RainbowId&filetype=5://239160774029742080    --另外一个材质
    else
        --修改材质方法2 新建一个材质,并设置该材质 (一般不这样使用)
        local mat_green  = Enum.MaterialTemplate.Material_green
        obj.MaterialType = mat_green
    end


    --[[
    -- 设置旋转,使变换的y轴沿着全局y轴,变换的z轴沿着全局z轴
    -- 计算出一个四维向量Quaternion, 难度较大
    local newq = Quaternion.New(0,0,2,1)
    local dir  = Vector3.New(0,0,1)
    local up   = Vector3.New(0,1,0)
    local angle = 45;  --角度
    local rotation_, ret1, ret2 = newq:RotateAxisAngle(dir, up, angle);  -- return Quaternion
    log( 'case1 ', obj.Rotation,  obj.LocalRotation, rotation_,  ret1, ret2 )
    --obj.LocalRotation = rotation_
    ]]

end

            
-- 设置一个触发器方块,碰撞后进行变色,位置修改

function  MTestcase.case2()
    print( "MTestcase.case2" );

    -- 增加一个cube立方体
    local obj = SandboxNode.new('GeoSolid', game.WorkSpace )
    obj.Name = 'cube_test2'
    obj.GeoSolidShape = Enum.GeoSolidShape.Cuboid   --Cuboid立方体

    obj.LocalPosition = Vector3.new( 100, 300, 100 );       --位置
    obj.Color         = ColorQuad.new( 255, 0, 0, 255 );    --红色


    obj.Size          = Vector3.new( 200, 200, 200 );       --碰撞盒子的大小

    -- 增加碰撞回调函数
    obj.Touched:connect( function(node, pos1, normal1 )
        if  node  then
            print("touched:", node , node.Name, pos1, normal1 )
            -- 碰撞的时候随机改变颜色
            obj.Color = ColorQuad.new( math.random(0,255), math.random(0,255), math.random(0,255), 255 );

            -- 碰撞的时候按方向改变位置
            local obj_pos = obj.LocalPosition;
            obj.LocalPosition = Vector3.new( obj_pos.x + normal1.x*50, obj_pos.y, obj_pos.z + normal1.z*50 );
        end
    end)
end
            
-- 场景中增加一个NPC模型,播放动作,行走 (只能在客户端侧执行)

function MTestcase.case3()
    print( "MTestcase.case3" );

    -- 增加一个模型
    local obj = SandboxNode.new('Model', game.WorkSpace )
    obj.Name    = 'model_test3'


    obj.ModelId = 'sandboxSysId://entity/100028/body.omod'    --增加一个野人
    --obj.ModelId = 'sandboxSysId://entity/100113/body.omod'    --增加一个野人战士
    --obj.ModelId = 'sandboxSysId://entity/100114/body.omod'    --增加一个野人伍长
    --obj.ModelId = 'sandboxSysId://entity/100063/body.omod'    --增加一个野人祭司 雷电祭司
    --obj.ModelId = 'sandboxSysId://entity/100117/body.omod'    --增加一个野人投矛手



    obj.LocalPosition = Vector3.new( 200, 500, 200 );       --位置
    obj.Anchored      = false                               --位置不固定
    --obj.Size          = Vector3.new( 200, 200, 200 );       --碰撞盒子的大小
    obj.Size          = Vector3.new( 150,  160, 150 )         --碰撞盒子的大小
    obj.Center        = Vector3.new( 0,    80,    0 )         --盒子中心位置

    wait(1)

    local anim_seq = 0     --用来表示多个动作中的第N个

    -- 增加碰撞回调函数
    obj.Touched:connect( function(node, pos1, normal1 )
        if  node  then
            print("touched:", node , node.Name, pos1, normal1 )

            --obj.Velocity = Vector3.new( normal1.x*50, 0, normal1.z*50 );   --行走效果

            local obj_pos = obj.LocalPosition;
            obj.LocalPosition = Vector3.new( obj_pos.x + normal1.x*10, obj_pos.y, obj_pos.z + normal1.z*10 );

            -- 改动朝向,这里可以注释掉
            --local face_position = obj.Position + normal1 * 100;
            --LookAtTarget( obj, Vector3.new( face_position.x,  0,  face_position.z ) )


            local animationIDs = obj:GetAnimationIDs()                  -- 新动作管理类
            print("animationIDs:", node , node.Name, animationIDs )
            --print_table( animationIDs, 'GetAnimationIDs' )


            -- 注意:
            -- 只有客户端代码可以加载一个模型的动作,服务器不会去加载动作文件
            -- 因此只有客户端可以拿到模型的所有动作列表

            -- 但是在已知动作列表名字的情况下
            -- 客户端和服务器都可以调用 PlayAnimation, 来播放和控制模型动作

            local legacy_animation = obj:GetLegacyAnimation()                  -- 旧版动作管理类
            if  legacy_animation then
                local anim_name_list   = legacy_animation:GetAnimationIDs()    -- 动作名字列表

                print_table( animationIDs, 'Legacy' )

                -- 所有动作列表
                -- 1=100100(stand) 2=100130(下蹲) 3=100101(run) 4=100111 5=100105(attack) 6=100112 7=100109 8=100200 9=100113 
                -- 100124(jump die)  100114(eat)  100115(摇头)  100102(sleep)  100106(die)  100107(be_attacked)

                anim_seq = anim_seq + 1              --每次播放不同的动作
                if  anim_seq > #anim_name_list then
                    anim_seq = 1
                end

                print( 'print animation', anim_seq, anim_name_list[ anim_seq ] )
                obj:StopAllAnimation(false);
                obj:PlayAnimation( '' .. anim_name_list[ anim_seq ],  1.0,  0 );   --播放动作
            end

        end
    end)

end                
            
-- 测试GUI界面 建立一个按钮,按钮点击后,打开一个选择框

function MTestcase.case4()
    print( "MTestcase.case4" );

    --创建一个新的ui容器 ui_root, 其他所有图片和按钮都放置在里面
    local root = SandboxNode.New('UIRoot', game.WorkSpace)
    root.Name = 'ui_root'


    -- 建立一个风格一致的按钮
    local function create_btn( root_parent )
        local tmp_button = SandboxNode.New('UIButton', root_parent )
        tmp_button.Icon  = 'RainbowId&filetype=5://230837294905430016'   --按钮背景
        tmp_button.FillColor = ColorQuad.new( 0,0,0,0 )
        tmp_button.TitleSize = 20
        tmp_button.DownEffect=Enum.DownEffect.ColorEffect
        return tmp_button
    end


    --在ui_root上创建按钮btn
    local button = create_btn( game.WorkSpace.ui_root )
    button.Title = "选择界面"
    button.DownEffect = Enum.DownEffect.ScaledEffect
    button.Size     =  Vector2.New(100, 50)
    button.Position =  Vector2.New(100, 100)



    local  callback_press_btn    --点击按钮的回调函数
    local  select_ui             --选择界面

    button.Click:Connect(function()
        print("点击打开选择界面")
        callback_press_btn()
    end)


    --打开界面
    callback_press_btn = function ()

        if  select_ui then
            --界面已经建立,切换隐藏或者显示
            if  select_ui.Visible == true then
                select_ui.Visible = false
            else
                select_ui.Visible = true
            end
        else
            --界面不存在,建立新的
            --背景图片
            select_ui = SandboxNode.New('UIImage', game.WorkSpace.ui_root)
            select_ui.Name = 'image'
            select_ui.Visible = true
            select_ui.Icon = 'RainbowId&filetype=5://230930788571418624'  --背景
            select_ui.Size     = Vector2.New(500, 500)
            select_ui.Position = Vector2.New(300, 100)
            select_ui.Pivot    = Vector2.New(0, 0)        --背景起始点

            select_ui.FillColor = ColorQuad.new( 255,255,255,128 )


            --建立两个按钮
            local button1 = create_btn( select_ui )
            button1.Title = "选择1"
            button1.Size     = Vector2.New(300, 120)
            button1.Position = Vector2.New(250, 100)

            local button2 = create_btn( select_ui )
            button2.Title = "选择2"            
            button2.Size     = Vector2.New(300, 120)
            button2.Position = Vector2.New(250, 300)


            local bubble_id = -1   --聊天气泡id
            button1.Click:Connect(function()
                print("点击1")
                local chat = game:GetService("Chat")
                bubble_id = chat:ShowChatBubble( '点击 1', true, 1, Vector3.new(0, 500, 0), bubble_id )
                select_ui.Visible = false
            end)

            button2.Click:Connect(function()
                print("点击2")
                local chat = game:GetService("Chat")
                bubble_id = chat:ShowChatBubble( '点击 2', true, 1, Vector3.new(0, 500, 0), bubble_id )
                select_ui.Visible = false
            end)

        end

    end

end
            
-- 新建一个NPC,使用Act组件,NPC随机行走

function MTestcase.case5()
    print( "MTestcase.case5" );

    local data_store = {}    --用来保存各种变量,传递参数给其他用例

    -- 增加一个actor
    local actor_yeren = SandboxNode.new('Actor', game.WorkSpace )     --actor会按照迷你游戏来表现行为和动画
    actor_yeren.Name    = 'model_yeren1'
    actor_yeren.ModelId = 'sandboxSysId://entity/100063/body.omod'    --增加一个野人祭祀


    actor_yeren.LocalPosition = Vector3.new( 500, 300, 800 );       --位置
    actor_yeren.Size          = Vector3.new( 400, 200, 400 )        --碰撞盒子的大小
    actor_yeren.Center        = Vector3.new( 0,   100,  0 )         --盒子中心位置

    data_store.actor_yeren = actor_yeren

    wait(1)

    local function cb_walk ()
        local move_pos = Vector3.new( 5000 - math.random()*10000, 300, 5000 - math.random()*10000 )   --随机行动方向
        print("NavigateTo:", move_pos )
        actor_yeren:NavigateTo( move_pos )      --向随机的位置寻路行走
        --actor_yeren:MoveTo( move_pos )      --向随机的位置直接行走
    end


    -- 一个定时器,  每N秒让 actor 行走
    local timer = SandboxNode.New("Timer", game.WorkSpace)
    timer.Delay = 1     -- 延迟多少秒开始
    timer.Loop = true   -- 是否循环
    timer.Interval = 10 -- 循环间隔多少秒
    timer.Callback = cb_walk
    timer:Start()     -- 启动定时器
    print("timer start")
    data_store.timer = timer


    return data_store;    -- 给其他用例使用
end
                
            
-- 玩家走近NPC后,NPC定住,弹出GUI界面

function MTestcase.case6()
    print( "MTestcase.case6" );
    local data_store_case5 = MTestcase.case5()


    local actor_yeren = data_store_case5.actor_yeren
    local timer       = data_store_case5.timer

    print("actor_yeren  :", actor_yeren )
    print("timer  :", timer )


    local gui_test   -- 保存界面

    --打开界面
    local open_GUI = function ()
        if  gui_test then
            --界面已经建立,切换隐藏或者显示
            if  gui_test.Visible == true then
                gui_test.Visible = false
            else
                gui_test.Visible = true
            end
        else
            print( 'open_GUI' )
            --创建一个新的ui容器 ui_root, 其他所有图片和按钮都放置在里面
            local root = SandboxNode.New('UIRoot', game.WorkSpace)
            root.Name = 'ui_root'

            --背景图片
            gui_test = SandboxNode.New('UIImage', game.WorkSpace.ui_root)
            gui_test.Name = 'image'
            gui_test.Visible = true
            gui_test.Icon = 'RainbowId&filetype=5://230930788571418624'  --背景
            gui_test.Size     = Vector2.New(500, 400)
            gui_test.Position = Vector2.New(300, 200)
            gui_test.Pivot    = Vector2.New(0, 0)        --背景起始点

            gui_test.FillColor = ColorQuad.new( 255,255,255,128 )


            -- 建立一个风格一致的按钮
            local function create_btn( root_parent )
                local tmp_button = SandboxNode.New('UIButton', root_parent )
                tmp_button.Icon  = 'RainbowId&filetype=5://230837294905430016'   --按钮背景
                tmp_button.FillColor = ColorQuad.new( 0,0,0,0 )
                tmp_button.TitleSize = 20
                tmp_button.DownEffect=Enum.DownEffect.ColorEffect
                return tmp_button
            end


            --建立按钮
            local button1 = create_btn( gui_test )
            button1.Icon = 'RainbowId&filetype=5://230837040806105088'
            button1.Title = "野人: 你好~~"
            button1.Size     = Vector2.New(400, 120)
            button1.Position = Vector2.New(250, 100)

            local button2 = create_btn( gui_test )
            button2.Title = "购买长矛"
            button2.Size     = Vector2.New(150, 90)
            button2.Position = Vector2.New(120, 300)

            button2.Click:Connect(function()
                button1.Title = '你购买了' .. math.floor(math.random()*10) + 1 ..  '个长矛~~';
            end)

            local button3 = create_btn( gui_test )
            button3.Title = "拜 拜"
            button3.Size     = Vector2.New(150, 120)
            button3.Position = Vector2.New(380, 300)

            button3.Click:Connect(function()
                gui_test.Visible = false
            end)

        end
    end


    -- 增加碰撞回调函数
    actor_yeren.Touched:connect( function(node, pos1, normal1 )
        if  node  then
            print("touched :", node , node.Name )
            if  node.Name == 'player1' then
                print("touched player1 :", node, node.Name )
                actor_yeren:StopNavigate()   -- 停止野人当前行走

                if  timer:GetRunState() == Enum.TimerRunState.RUNNING then
                    timer:Pause()       --暂停定时器
                end

                open_GUI()
            end
        end
    end )

end                
            
-- 根据GUI按钮NPC选择不同逻辑

function MTestcase.case7()
    print( "MTestcase.case7" );

    -- 增加一个actor
    local actor_yeren = SandboxNode.new('Actor', game.WorkSpace )     --actor会按照迷你游戏来表现行为和动画
    actor_yeren.Name    = 'model_yeren1'
    actor_yeren.ModelId = 'sandboxSysId://entity/100028/body.omod'    --增加一个野人


    actor_yeren.LocalPosition = Vector3.new( 500, 300, 500 );       --位置
    actor_yeren.Size          = Vector3.new( 400, 200, 400 );       --碰撞盒子的大小
    actor_yeren.Center        = Vector3.new( 0,   100,  0 )         --盒子中心位置


    wait(1)

    local gui_test   -- 保存界面

    --打开界面
    local open_GUI = function ()
        if  gui_test then
            --界面已经建立,切换隐藏或者显示
            if  gui_test.Visible == false then
                gui_test.Visible = true
            end
        else
            print( 'open_GUI' )
            --创建一个新的ui容器 ui_root, 其他所有图片和按钮都放置在里面
            local root = SandboxNode.New('UIRoot', game.WorkSpace)
            root.Name = 'ui_root'

            --背景图片
            gui_test = SandboxNode.New('UIImage', game.WorkSpace.ui_root)
            gui_test.Name = 'image'
            gui_test.Visible = true
            gui_test.Icon = 'RainbowId&filetype=5://230930788571418624'  --背景
            gui_test.Size     = Vector2.New(500, 400)
            gui_test.Position = Vector2.New(300, 200)
            gui_test.Pivot    = Vector2.New(0, 0)        --背景起始点

            gui_test.FillColor = ColorQuad.new( 255,255,255,128 )


            -- 建立一个风格一致的按钮
            local function create_btn( root_parent )
                local tmp_button = SandboxNode.New('UIButton', root_parent )
                tmp_button.Icon  = 'RainbowId&filetype=5://230837294905430016'   --按钮背景
                tmp_button.FillColor = ColorQuad.new( 0,0,0,0 )
                tmp_button.TitleSize = 20
                tmp_button.DownEffect=Enum.DownEffect.ColorEffect
                return tmp_button
            end


            --建立按钮
            local button1 = create_btn( gui_test )
            button1.Icon = 'RainbowId&filetype=5://230837040806105088'
            button1.Title = "野人: 你好~~"
            button1.Size     = Vector2.New(400, 120)
            button1.Position = Vector2.New(250, 100)


            local function attack( dir )
                actor_yeren.LocalEuler = Vector3.new( 0, dir * -90, 0)  --改变朝向
                actor_yeren:PlayAnimation( '100105', 1.0, 1 )           --投掷动作

                wait(0.5)

                -- 可以这样新建一个长矛
                local changmao = SandboxNode.new('Model', game.WorkSpace )
                changmao.Name     = 'model_changmao'
                changmao.Anchored = false
                changmao.ModelId  = 'sandboxSysId://itemmods/12004/body.omod'

                -- 或者先摆放一根长矛,再克隆出来
                --local changmao = game.WorkSpace.changmao:Clone()
                --changmao.Parent = game.WorkSpace;

                changmao.LocalPosition = Vector3.new(actor_yeren.LocalPosition.x, actor_yeren.LocalPosition.y + 150, actor_yeren.LocalPosition.z)
                changmao.Velocity      = Vector3.new( 2000 * dir, 100, 0 )

                --调整方向
                changmao.Euler = Vector3.new( actor_yeren.Euler.x - 90 , actor_yeren.Euler.y, actor_yeren.Euler.z )
                --print  ( 'Euler: ', actor_yeren.Euler,  ' ', changmao.Euler )

                wait(10)
                changmao:Destroy()    --清理节点(节点会挂在game.WorldSpace下,destory后节约内存)
            end


            local button2 = create_btn( gui_test )
            button2.Title = "长矛攻击左边"
            button2.Size     = Vector2.New(150, 90)
            button2.Position = Vector2.New(120, 300)

            button2.Click:Connect(function()
                gui_test.Visible = false
                attack(-1)
            end)


            local button3 = create_btn( gui_test )
            button3.Title = "长矛攻击右边"
            button3.Size     = Vector2.New(150, 90)
            button3.Position = Vector2.New(380, 300)

            button3.Click:Connect(function()
                gui_test.Visible = false
                attack(1)
            end)

        end
    end



    -- 增加碰撞回调函数
    actor_yeren.Touched:connect( function(node, pos1, normal1 )
        if  node  then
            print("touched :", node , node.Name )
            if  node.Name == 'player1' then
                print("touched player1 :", node, node.Name )
                actor_yeren:StopNavigate()   -- 停止野人当前行走


                open_GUI()
            end
        end
    end )

end
                                
            
-- 生成一个建筑物,增加一个门,碰撞打开,自动关闭

function MTestcase.case8()
    print( "MTestcase.case8" );

    -- 增加一个cube立方体
    local door_move = SandboxNode.new('GeoSolid', game.WorkSpace )
    door_move.Name = 'door_move'
    door_move.GeoSolidShape = Enum.GeoSolidShape.Cuboid   --Cuboid立方体   Cylinder圆柱体

    door_move.LocalPosition = Vector3.new( 750, 250, 500 );     --位置
    door_move.LocalScale    = Vector3.new( 5, 5, 1 );           --缩放

    local TweenService = game:GetService("TweenService")
    local tweenInfo = TweenInfo.New(
        1,    -- Time
        Enum.EasingStyle.Linear,   -- EasingStyle
        Enum.EasingDirection.Out,  -- EasingDirection
        0     -- DelayTime
        -1,   -- RepeatCount (小于零时 tween 会无限循环)                
        true  -- Reverses (tween 完成目标后会反转)
    )

    local tween      = TweenService:Create(door_move, tweenInfo, {Position = Vector3.New(750, 600, 500)})  --升高动画
    local tween_back = TweenService:Create(door_move, tweenInfo, {Position = Vector3.New(750, 250, 500)})  --降低动画

    door_move.Touched:Connect( function(node, pos1, normal1 )
        if  node.Name == 'player1' then
            --print("door touched player1 :", node, node.Name )
            tween:Play()        --升高
            wait(5)
            tween_back:Play()   --降低
        end
    end )
end
                
            
-- NPC判断与玩家的距离,靠近或者攻击

function MTestcase.case9()
    print( "MTestcase.case9" );

    -- 增加一个actor
    local actor_yeren = SandboxNode.new('Actor', game.WorkSpace )     --actor会按照迷你游戏来表现行为和动画
    actor_yeren.Name    = 'model_yeren1'
    actor_yeren.ModelId = 'sandboxSysId://entity/100028/body.omod'    --增加一个野人


    actor_yeren.LocalPosition = Vector3.new( 500, 300, 500 );       --位置
    actor_yeren.Size          = Vector3.new( 50,  200,  50 )        --碰撞盒子的大小
    actor_yeren.Center        = Vector3.new( 0,   100,  0 )         --盒子中心位置

    wait(1)


    local player1 = game:GetService("Players").player1
    print( 'player1', player1 )


    local function cb_walk_attack ()
        --距离大于15米就走近, 小于15米就攻击

        print("cb_walk_attack:", ( actor_yeren.Position - player1.Position ).length )
        if ( actor_yeren.Position - player1.Position ).length > 1500 then
            --向玩家靠近
            print("NavigateTo:", player1.Position )
            actor_yeren:NavigateTo( player1.Position )
        else
            actor_yeren:StopNavigate()

            --朝向玩家
            local direction = actor_yeren.Position - player1.Position
            direction.y = 0
            local _, rotation = Quaternion.LookAt(direction)
            actor_yeren.LocalRotation = rotation


            --扔出一根长矛
            actor_yeren:PlayAnimation( '100105', 1.0, 1 )  --投掷动作
            wait(0.5)
            local changmao = game.WorkSpace.changmao:Clone()
            changmao.Name     = 'model_changmao'
            changmao.Parent = game.WorkSpace;


            --长矛的位置是野人的右手边,计算右边后调整位置
            local VECUP = Vector3.new(0, 1, 0)
            local right_hand = direction:cross(VECUP)   --VECUP
            Vector3.Normalize( right_hand )

            changmao.LocalPosition = Vector3.new( actor_yeren.LocalPosition.x + right_hand.x*64,
                actor_yeren.LocalPosition.y + 200,
                actor_yeren.LocalPosition.z + right_hand.z*64 )

            changmao.Velocity = (player1.Position - actor_yeren.Position)*1.5 + Vector3.new( 0, 200, 0 )

            --调整方向
            changmao.Euler = Vector3.new( actor_yeren.Euler.x - 90 , actor_yeren.Euler.y, actor_yeren.Euler.z )
            --print  ( 'Euler: ', actor_yeren.Euler,  ' ', changmao.Euler )

            wait(10)
            changmao:Destroy()

        end

    end


    -- 一个定时器,  每N秒让野人判断一次行动
    local timer = SandboxNode.New("Timer", game.WorkSpace)
    timer.Delay = 1     -- 延迟多少秒开始
    timer.Loop = true   -- 是否循环
    timer.Interval = 3  -- 循环间隔多少秒
    timer.Callback = cb_walk_attack
    timer:Start()       -- 启动定时器
    print("timer start")

end
            
-- 给玩家增加一个攻击按钮,点击的时候会向前方攻击

function MTestcase.case10()
    print( "MTestcase.case10" );

    local player1 = game.WorkSpace.player1
    print( 'player1', player1 )

    wait(2)

    --给模型设置绑点
    local attachment= SandboxNode.New('BindAttachment', player1)
    attachment.Name = "rightHand"
    attachment.BoneName = '101'
    attachment.LocalPosition = Vector3.New(0, 0, 0)
    attachment.LocalScale = Vector3.New(1, 1, 1)
    print( 'attachment', attachment )

    -- 增加一个长矛模型 绑定在手上
    local changmao_bind = SandboxNode.new('Model', attachment )
    changmao_bind.Name    = 'model_changmao'
    changmao_bind.ModelId = 'sandboxSysId://itemmods/12004/body.omod'
    changmao_bind.EnablePhysics = false
    changmao_bind.CanCollide = false
    changmao_bind.CanTouch = false
    changmao_bind.LocalPosition = Vector3.new(0,0,0)
    changmao_bind.LocalEuler    = Vector3.new(0,90,0)

    print( 'changmao_bind', changmao_bind )


    wait(1)

    -- 增加一个按钮
    local ui_root = SandboxNode.New('UIRoot', game.WorkSpace)
    ui_root.Name = 'ui_root'

    local attack_button = SandboxNode.New('UIButton', ui_root )
    attack_button.Icon  = 'RainbowId&filetype=5://230837040806105088'   --按钮背景
    attack_button.FillColor = ColorQuad.new( 0,0,0,0 )
    attack_button.TitleSize = 20
    attack_button.Title = "attack"
    attack_button.DownEffect = Enum.DownEffect.ScaledEffect

    attack_button.LayoutHRelation = Enum.LayoutHRelation.Right   --靠右
    attack_button.LayoutVRelation = Enum.LayoutVRelation.Bottom  --靠下
    attack_button.Size            = Vector2.New(100, 100)

    local screen_size =  game:GetService( 'WorldService' ):GetUISize()
    attack_button.Position   =  Vector2.New( screen_size.x - 260,  screen_size.y - 100)


    attack_button.Click:Connect( function()
        print("attack")

        --扔出一根长矛
        player1:PlayAnimation( '100105', 1.0, 1 )  --投掷动作
        wait(0.5)

        -- 获得玩家当前的朝向
        local v3_direction = player1.Rotation:LookDir();

        --长矛的位置是右手边,计算右边后调整位置
        local VECUP = Vector3.new(0, 1, 0)
        local right_hand = v3_direction:cross(VECUP)   --VECUP
        Vector3.Normalize( right_hand )
        --print( 'player1.LocalPosition=', player1.LocalPosition, right_hand )

        local changmao = game.WorkSpace.changmao:Clone()
        changmao.Parent = game.WorkSpace;
        changmao.CollideGroupID = 1

        changmao.LocalPosition = Vector3.new( player1.LocalPosition.x + right_hand.x*64,
            player1.LocalPosition.y + 120,
            player1.LocalPosition.z + right_hand.z*64 )

        --changmao.LocalEuler = Vector3.new( player1.LocalEuler.x - 90, player1.LocalEuler.y, player1.LocalEuler.z )
        changmao.Euler = Vector3.new( player1.Euler.x - 90 , player1.Euler.y, player1.Euler.z )

        print( 'changmao Euler=', changmao.Euler, ' ', changmao.LocalEuler )


        local direction   = player1.Rotation:LookDir();
        --print( 'player1.LocalPosition=', player1.LocalPosition, right_hand )
        changmao.Velocity = direction * -2000 + Vector3.new( -500, 200, 0 );    -- 目前z轴为负,需要乘负数来转换

        wait(10)
        changmao:Destroy()
    end )

end                
            
-- 序列化一个table数据,并存在云端

function MTestcase.case11()
    print( "MTestcase.case11" );

    -- 增加一个cube立方体
    local obj = SandboxNode.new('GeoSolid', game.WorkSpace )
    obj.Name = 'cube_test3'
    obj.GeoSolidShape = Enum.GeoSolidShape.Cuboid   --Cuboid立方体

    obj.LocalPosition = Vector3.new( 0, 300, 0 );           --位置
    obj.Color         = ColorQuad.new( 0, 0, 255, 255 );    --蓝色


    local table_name_key = 'key1234567_obj_position1'    --存取的key值


    -- 取云端数据
    local CloudService = game:GetService("CloudService")
    local ret1, table_data = CloudService:GetTable( table_name_key )
    print( '====GetTable ', ret1, ' , ', table_data )

    if  ret1 and type(table_data) == 'table' then
        print_table( table_data, 'ret_table= ' )    --成功取到了数据
    else
        --取不到则新建数据  false, invalid base64 data
        table_data = { msg='test', value=1, pos={ x=300, y=300, z=300 } }
        print_table( table_data, 'new create ret_table= ' )       --首次新建
    end
    table_data.value = table_data.value + 1       --每次运行:保存的value值递增1
    table_data.pos.y = table_data.pos.y + 100     --每次运行:坐标y值每次递增100

    obj.LocalPosition = Vector3.new( table_data.pos.x, table_data.pos.y, table_data.pos.z )   --使用数据

    --存数据到云端
    local ret2 = CloudService:SetTable( table_name_key, table_data )
    print( '====SetTable ', ret2 )

end
                                
            
-- 序列化一个table数据,并存在云端(异步模式)

function MTestcase.case12()
    print( "MTestcase.case12" );

    -- 增加一个cube立方体
    local obj = SandboxNode.new('GeoSolid', game.WorkSpace )
    obj.Name = 'cube_test3'
    obj.GeoSolidShape = Enum.GeoSolidShape.Cuboid   --Cuboid立方体

    obj.LocalPosition = Vector3.new( 0, 300, 0 );           --位置
    obj.Color         = ColorQuad.new( 0, 0, 255, 255 );    --蓝色

    local table_name_key = 'key1234567_obj_position2'    --存取的key值

    -- 取云端数据
    local CloudService = game:GetService("CloudService")
    CloudService:GetTableAsync( table_name_key,
        -- GetTableAsync的回调函数
        function( ret_read, table_data )
            print( '====GetTable1 ', ret_read, ' , ', table_data )

            if  ret_read and type(table_data) == 'table' then
                print_table( table_data, 'ret_table= ' )    --成功取到了数据
            else
                --取不到则新建数据  false, invalid base64 data
                table_data = { msg='test', value=1, pos={ x=300, y=300, z=300 } }
                print_table( table_data, 'new create ret_table= ' )       --首次新建
            end

            table_data.value = table_data.value + 1       --每次运行:保存的value值递增1
            table_data.pos.y = table_data.pos.y + 100     --每次运行:坐标y值每次递增100

            obj.LocalPosition = Vector3.new( table_data.pos.x, table_data.pos.y, table_data.pos.z )   --使用数据


            --存数据到云端
            CloudService:SetTableAsync( table_name_key, table_data,
                --SetTableAsync的回调函数
                function( ret_save )
                    print( '====SetTable1 ', ret_save )
                end
            );

        end
    );

end                
            
-- 建立一个立方体区域,判断物体进入

function MTestcase.case13()
    print( "MTestcase.case13" );

    local area = SandboxNode.New('Area', game.WorkSpace) --创建AreaNode节点

    --创建Actor实例
    --local actorNode = SandboxNode.New('Actor')
    --actorNode.Name = "my_actor"

    area.Beg = Vector3.new( -100, 0,   -100 );
    area.End = Vector3.new(  800, 500,  800 );
    area.EffectWidth = 16;
    area.Show = true   --显示


    --actorNode进入区域
    area.EnterNode:Connect(function(node)
        print("node 进入区域", node)
    end)

    area.LeaveNode:Connect(function(node) 
        print("Node 离开区域", node)
    end)
end

            
-- 鼠标点击选中物体判断 pickup

function MTestcase.case14()
    print( "MTestcase.case14" );


    --使用屏幕中心点点击,使用 camera_win_size
    --local camera_win_size = game.WorkSpace.CurrentCamera.WindowSize
    --local center_pos_x = camera_win_size.x * 0.5
    --local center_pos_y = camera_win_size.y * 0.5

    local inputservice = game:GetService("UserInputService")

    --按下
    local function inputBegan( inputObj, bGameProcessd )
        log( "InputBegan", inputObj, bGameProcessd, inputObj.UserInputState, inputObj.UserInputType )

        --print( inputObj.UserInputState )   -- 0 这里都是InputBegan 
        if inputObj.UserInputType == Enum.UserInputType.Keyboard.Value then
            print( 'keyPressed KeyCode: ', inputObj.KeyCode ) -- 鼠标左键按下时位置
        end

        if inputObj.UserInputType == Enum.UserInputType.MouseButton1.Value then
            print( 'left pressed pos: ', inputObj.Position.x, ' ', inputObj.Position.y ) -- 鼠标左键按下时位置

            local obj_list = {     --表示只在哪些obj里面查找
                --game.WorkSpace.g0,
                --game.WorkSpace.g0.g0_copy_2,
            }

            local rets = inputservice:PickObjects( inputObj.Position.x,  inputObj.Position.y, obj_list )
            log( 'GetCursorPick[', rets, '] [', obj_list, ']' )

            if  rets then
                --改动框的显示,明确是否被选中
                if  rets.CubeBorderEnable == false then
                    rets.CubeBorderEnable = true
                else
                    rets.CubeBorderEnable = false
                end
            end

        end
    end
    inputservice.InputBegan:Connect(inputBegan)


    --移动
    --local function onInputChanged( inputObj, passed )
        --gg.log ( '2 onInputChanged', inputObj, inputObj.UserInputType, passed )
    --end
    --inputservice.InputChanged:Connect( onInputChanged )


    --抬起
    --local function onInputEnded( inputObj, passed )
        --gg.log ( '3 onInputEnded', inputObj, inputObj.UserInputType, passed )
    --end
    --inputservice.InputEnded:Connect(   onInputEnded   )

end

            
-- 检查射线ray碰撞物体

function MTestcase.case15()
    print( "MTestcase.case15" );


    --按下空格键的时候,就发出一条射线
    local function test_function()

        --从玩家所在点发出射线
        --如果使用camera射线,则 game.WorkSpace.player1 改为 game.WorkSpace.Camera
        local position = game.WorkSpace.player1.LocalPosition
        local orgin = Vector3.new( position.x, position.y+100, position.z )   --玩家坐标在脚底,抬高1米


        --玩家面向的方向为射线的方向
        --方法1: 取玩家的方向四元数,再转换成标准向量
        --local q4_rotate = game.WorkSpace.player1.Rotation    -- Quaternion 玩家朝向四元数
        --log( 'Rotation=1=', q4_rotate )


        --方法2: 取玩家的欧拉角vector3,再转换成四元数,再转换成标准向量
        local vec3_euler = game.WorkSpace.player1.Euler
        local q4_rotate = Quaternion.FromEuler( vec3_euler.x, vec3_euler.y, vec3_euler.z )
        --log ( 'Euler=2=', vec3_euler, q4_rotate )

        local vec3_direction = q4_rotate:LookDir()  *  -1   -- 四元数转成vector3向量 (左右手坐标系转换,需要乘以负1)

        --RaycastClosest  射线检测最近物体
        --RaycastAll      射线检测所有碰到的物体
        --参数1 射线发出点
        --参数2 射线发出方向
        --参数3 射线距离
        --参数4 CollideGroupID碰撞组id过滤  { id1, id2, id3 }
        local ret_table = game:GetService( 'WorldService' ):RaycastAll( orgin, vec3_direction, 3200, true, {1,2,3,4,5} )
        log ( 'debug_check_ray=3=', orgin, vec3_direction, ret_table )


        print_table( ret_table, 'all_ray_objs' )    --打印整个table(所有射线碰到的物体)
        --返回值
        --{ 1={normal={0, 0, -0} distance=620 position={-550, 100, 818} 
        --obj=[node(67930)(Model):野人伍长]} 2={normal={0, 0, -1} distance=1409 position={-845, 100, 1550} 
        --obj=[node(67926)(GeoSolid):g0_copy墙壁 ]} } 

    end



    -- 按空格键触发上面的测试函数
    local function inputBegan( inputObj, bGameProcessd )
        if inputObj.UserInputType == Enum.UserInputType.Keyboard.Value then
            if  inputObj.KeyCode  == Enum.KeyCode.Space.Value then    -- Enum.KeyCode.Space.Value = 32
                test_function()
            end
        end
    end
    game:GetService("UserInputService").InputBegan:Connect(inputBegan)

end                
            
-- 播放声音特效

function MTestcase.case16()
    print( "MTestcase.case16" );


    local function test_function()
        local sound = SandboxNode.New('Sound', game.WorkSpace )           --创建Sound节点
        sound.SoundPath = 'sandboxSysId://sounds/ent/yanhua/blast2.ogg'   --设置资源路径 (气泡音)

        sound.IsLoop = false    --是否循环播放
        --sound.RollOffMode = Enum.RollOffMode.Linear --设置声音衰减模式
        --sound.RollOffMinDistance = 300
        --sound.RollOffMaxDistance = 700

        sound:PlaySound()  --播放函数
        print( 'client playSoundEffect');

        --播放结束事件回调函数
        sound.PlayFinish:Connect(function(node)
            node:Destroy()
            print("sound is Destroy")
        end)
    end



    -- 按空格键触发上面的测试函数
    local function inputBegan( inputObj, bGameProcessd )
        if inputObj.UserInputType == Enum.UserInputType.Keyboard.Value then
            log( 'keyPressed KeyCode: ', inputObj.KeyCode, bGameProcessd )
            if  inputObj.KeyCode == Enum.KeyCode.Space.Value then    -- Enum.KeyCode.Space.Value = 32
                test_function()
            end
        end
    end
    game:GetService("UserInputService").InputBegan:Connect(inputBegan)

end