Raycast射线实现3D世界交互,如何实现立体界面UI

说明

本文为B站视频的配套文章,如果您觉得不错,请一定关注三联一波!

简介

大家好,我是Nowpaper
在一些3D游戏中,游戏界面是和3D世界存在互动关系,它们不再是在屏幕上2D显示,而是以3D的形式出现在游戏世界当中,这种帅气的UI如何在CocosCreator中实现呢?
动画

在3.0以前的版本中,实现是非常费劲的,而在Cocos Creator 3.0以后的版本,我们可以借助3D系统,完成3D效果的UI,在本文章当中,我将使用Creator实现常见的3D界面
动画1

资产准备

既然是3DUI,就得需要一些3DUI的素材,这个部分最好请专业的美术设计师制作一些,这篇帖子中的3D文件使用Blender来制作而来,包含了独立的材质和网格模型

为了更好的展示,需要一个基本的三维场景,在这里我将使用CocosStore中,两个免费素材,当然你可以使用自己的素材制作场景,当下载好了之后,解压资源包,这里我推荐直接用文件浏览器,打开项目Assets,拷贝对应的文件到里面,打开CocosCreator会执行编译扫描

创建项目

新建一个项目,我使用的是3.3.0的版本,打开HelloWolrd的Main场景
在这里为了更好的处理逻辑,我将几个按钮都使用独立的节点完成创建,就是自行添加MeshRender,新建一个Node节点,添加MeshRenderer组件,然后将导入的FBX文件中的网格拖动到其中,在材质栏中添加一个材质,拖动FBX文件中的材质球放到其中,完成后添加碰撞盒,并设置好碰撞区域


现在摆放一下它们,让它呈现立体感,并且加入Cocos的宇航员小人,为了更加好看,再调整一下布局,直到你感觉舒适了为止,我们的期望是,当点击Start按钮,镜头将会从骑士切换到太空人,点击back按钮,则会返回到骑士的镜头上

处理逻辑

下面实现一下点击触发逻辑,这里我们将需要两个脚本,一个用来处理按钮点击后的逻辑,一个用来检查用户的点击
新建第一个脚本,用来处理点击后的逻辑,这个我们就完成的简单一点,直接移动摄像机到指定的目标点,参数我们采用外部设置的方式,直接在Creator编辑器中填写,摄像机为了控制方便,我也将它引用进来,处理代码相对比较简单,使用Tween完成缓动动画

import { _decorator, Camera, Vec3, v3, tween, Component } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('MoveCameraOnButtonClick')
export class MoveCameraOnButtonClick extends Component {
   @property(Camera)
   camera:Camera = null;
   @property
   target:Vec3 = v3();
   onClick(){
       tween(this.camera.node).to(1,{position:this.target},{easing:"sineOut"}).start();
   }
}

现在建立第二脚本,这是用来处理点击判定的,所以稍微有一些复杂,它的原理是通过点击屏幕的时候,摄像机位置产生的射线,经过屏幕的点击坐标,然后看能射中那个3D物体,如果这个3D物体上,有按钮的脚本,就认定成功了,所以还要用到Canvas对象,来处理屏幕点击事件,在Start方法里,监听它的鼠标移动和点击抬起来的事件,实现按钮检查,通过screenPointToRay方法,获得一个射线,这里的代码就是通过物理射线的方式,取得到底是哪个3D物体被命中,如果命中,就对色值进行处理,为了简单,直接将色值做了预设指定,来方便调整,而且还得记录一下上一次的按钮是哪个,以便于修正处理,当鼠标移动的时候,如果没有检查到button,那么还得环境颜色设置成为默认值,如果检查到了,将选中的button材质的环境色设置成为命中色,我们需要对没有命中时候,取消到当前的按钮,而在MouseUp事件中,处理OnClick事件调用,执行里面的逻辑
具体代码如下:


import { _decorator, Component, Node, Camera, Canvas, EventMouse, geometry, Color, PhysicsSystem, MeshRenderer } from 'cc';
import { MoveCameraOnButtonClick } from './MoveCameraOnButtonClick';
const { ccclass, property } = _decorator;
@ccclass('UIButtonCheck')
export class UIButtonCheck extends Component {
    @property(Camera)
    camera: Camera = null;
    @property(Canvas)
    canvas: Canvas = null;
    private _cur_button:Node = null;
    private _color1:Color = Color.BLACK;
    private _color2:Color = new Color(5,29,127);

    start() {
        this.canvas.node.on(Node.EventType.MOUSE_MOVE, this.checkButton, this);
        this.canvas.node.on(Node.EventType.MOUSE_UP, this.onMouseUp, this);
    }
    private checkButton(event: EventMouse) {
        const outRay = new geometry.Ray();
        this.camera.screenPointToRay(event.getLocationX(),event.getLocationY(),outRay);
        if(PhysicsSystem.instance.raycast(outRay)){
            const button = PhysicsSystem.instance.raycastResults.find(
                (v)=>{return v.collider.getComponent(MoveCameraOnButtonClick)}
            );            
            if(button){
                if(button.collider.node == this._cur_button) return;
                if(this._cur_button){
                    this.setButtonColor(this._cur_button,this._color1);
                }
                this._cur_button = button.collider.node;
                this.setButtonColor(this._cur_button,this._color2);
            }else if(this._cur_button){
                this.setButtonColor(this._cur_button,this._color1);
                this._cur_button = null;
            }
        }else if(this._cur_button){
            this.setButtonColor(this._cur_button,this._color1);
            this._cur_button = null;
        }        
    }
    private onMouseUp(event: EventMouse) {
        if(this._cur_button){
            this._cur_button.getComponent(MoveCameraOnButtonClick).onClick();
        }
    }
    private setButtonColor(node:Node,color:Color){
        node.getComponent(MeshRenderer).getMaterial(0).setProperty("emissive",color);
    }
}

现在返回到Creator编辑器,为所有的按钮文字都添加一个第一个脚本,设置好对应的摄像机引用,和摄像机移动目标点,这里你需要移动摄像机来记录目标点,挨个设置一下

将第二个脚本,UIButtonCheck加入到场景,并且建立Canvas,以用来引用到按钮检查器当中,完成引用之后保存运行


看看效果,当鼠标移入到文字的时候,就能看到颜色变化,点击后,摄像机会跟着移动到目标

动画2

跟随式3D UI

第一种3DUI已经完成,更多的扩展,可以参考我的代码,但是我们会发现,交互按钮长驻在场景中,摄像机移动走了之后,就会失去按钮的可见性,那么有没有办法,让按钮元素跟随摄像机移动呢?这样就可以作整体UI,符合那种需要和3D场景脱离的3D UI,答案当然是可以的,
做法很简单,现在复制一下Main这个场景,我们在新的场景中实现,将文本按钮作一下调整,然后直接将它挂到摄像机的子节点下,然后它们就会跟随摄像机移动了


其实是非常简单的实现,在某一些游戏里,比如手持武器、跟随UI,都是这种实现方式来完成的
动画3

和2D交互

那么更进一步 ,如何和2D UI联动呢,这个其实非常简单,为了演示这个过程,咱们需要准备一个2D界面,我在这里使用的是,官方商城中的免费2D UI资源,商城里面基础素材还是足够丰富的


简单搭建一个弹窗,为了产生UI互动,先写一个UI处理,比如GameUI,实现以下简单的功能,两个方法来处理,隐蔽和显示指定Node

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('GameUI')
export class GameUI extends Component {
    
    @property(Node)
    panel:Node = null;

    onClickShowPanel(){
        this.panel.active = true;
    }
    onClickHidePanel(){
        this.panel.active = false;
    }
}

返回到Creator中,为Canvas添加GameUI的组件,并且将弹窗节点作为目标,操作完后将其隐蔽,当然了在那之前,我们需要给Close按钮,添加Cocos的Button组件,让它能够调用GameUI上的隐藏弹窗方法


我们将3DButton给封装一下,用来触发指定的游戏逻辑,额,类名是不支持数字开头,叫DButton也没什么关系,我们继续

import { _decorator, Component, Node, EventHandler } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('DButton')
export class DButton extends Component {
    @property([EventHandler])
    clickEvents:EventHandler[] = [];
    onClick(){
        EventHandler.emitEvents(this.clickEvents);
    }
}

这里将参考Cocos的Button组件的事件方式,写了一个onClick来触发它所引用到的事件,保存返回到Creator,给Option添加DButton的组件,并且指定好点击显示弹窗的事件,这个操作基本上和2D的button是一样的


但是它怎么能够被3DUI检查器触发处理呢,这时候就得需要一些面向对象的改造,首先,让移动摄像机的按钮脚本里面的类,继承自DButton,由于都有onClick方法,所以在处理onClick方法的时候,子类里的就会覆盖父类里的,

import { _decorator, Component, Node, Camera, Vec3, v3, tween } from 'cc';
import { DButton } from './3DButton';
const { ccclass, property } = _decorator; 
@ccclass('MoveCameraOnButtonClick')
export class MoveCameraOnButtonClick extends DButton {
   @property(Camera)
   camera:Camera = null;
   @property
   target:Vec3 = v3();

   onClick(){
       tween(this.camera.node).to(1,{position:this.target},{easing:"sineOut"}).start();
   }
}

改造一下UIButtonCheck.ts 脚本,



采用寻找和处理DButton类名,而不是之前的移动摄像机的脚本类名,这样的话,就通过面向对象的方法实现了点击调用,而且不用太大的代码重构,现在保存到项目中,运行一下看看效果,当点击Option的时候,就会弹出弹窗,点击关闭按钮,就会关闭它,看起来还是不错的

动画1

代码地址

在这个基础上,你可以做出很多的有趣的处理,实现更多的3DUI或者场景交互

https://github.com/Nowpaper/CC3DUI-Demo-For-Raycast

其他

本人喜欢研究各种有趣的玩法,以下是往期制作,可以移步研究

用RenderTexture实现Sprite版小地图和炫酷的传送门

好玩的编队代码,魔性队伍排列惊喜不断完全停不下来

手撸三个有关Bundle详细教程,大厅+子游戏模式从入门到进阶

Cocos3D《病毒传播模拟器》游戏版本1 开发日志和总结

案例开发 四图猜词 Part1~4 全集教程

2赞

mark!

mark! 6p又高产

真的赞 :smiley: