详解RenderToTexture的实用玩法!Cocos Creator实现FPS经典瞄准镜+监视器

引言:前两周,「Cocos Star Writer」Nowpaper 在《笼中窥梦》视错觉效果的实现中使用了 RenderToTexture 技术,本次 Nowpaper 将继续拓展 RenderToTexture 的使用法。

RenderToTexture 是个非常有趣的技术,它能够将一个摄像机画面渲染成纹理,然后和材质结合,让某一个 Mesh 显示成指定的画面。在游戏开发中,它被广泛用于实现镜子、监视器画面、瞄准镜、传送门,甚至用户界面显示、动态纹理喷涂等。

a25c6798d8d169f369b82ff9f43de625.gif

b2221a688a9ecc4725bc9f24d55ad802.gif

在 Cocos Creator 3.4 以后的版本中,RenderToTexture 技术已经相对完善,使用起来也更加方便。在我之前的传送门分享中,传送门里的画面就使用了这个技术来实现,当时还得写一些代码,而现在只需要在编辑器中编辑,就可以轻松实现可渲染纹理了。

今天我们将继续拓展一下,使用 RenderToTexture 做一个瞄准镜以及显示指定摄像机的画面监视器。

PS.源码和视频教程见文末。本次使用的是 v3.5。

准备

首先新建一个项目并搭建游戏场景。我这里就搭建一个简单的街道,包含一些物体,让场景看起来稍微不那么单调。

82ccad570d768ad55cc4a34f1bb3fdc9.jpeg

瞄准镜

一般来说当角色瞄准的时候,我们可以看到在镜头中,显示的画面被放大,视觉更加前向。

015eeaf78eb42de6b04854c3641c884d.jpeg

这个效果的原理其实就是:在枪械的瞄准位置增加一个摄像机,然后将摄像机的画面渲染到一个纹理上。

509b8448608305e7e3396816903b9d5f.png

这样的话事情就简单了。我准备了一个枪械,它带有瞄准动画,现在为他增加一个第一人称摄像机,通过调整让它看起来比较合适。

e63a78d0937402e349f1d987a43db3ba.jpeg

现在新建一个可渲染纹理:

7b9058e774e122db43cd6ca11fb3bff2.png

图像宽高数值默认都是1,这个需要我们依据自己的情况作修改,通常是按照视口的大小比例来调整。如果要想完美适配的话,最好是代码中作一些控制,在这里我们就直接使用 512x512:

ac9cb30c10cdb5f8ad74218a1579dd0e.png

现在再创建一个材质:

70d5e7aca02b98847922a8fd0c7b89a6.png

着色器选择为内置-unlit:

6fa48cc409aaf05b07c5e9e111cc9af0.png

开启 UseTexture,勾选 RTTexture 选项,将刚刚新建的可渲染纹理拖动进下面的引用中:

72b32ca9678c816aef0c760453ee9d68.jpeg

现在为瞄准镜建立一个圆形面片。一般来说建模师会提供一个,在这里我们就直接自己放个圆柱形,通过节点调整到对应的 Node 中:

17896dca9ed670fbc76bf951f8a5d369.jpeg

6cf78464519a0cfe42a0f5ac8e271dc5.jpeg

现在选择枪械的动画,去掉动画预烘培:

d324fe92b2e5d858414260d40a5b090f.png

关于动画控制脚本,在这里就不提供了,播放 Animation 的指定动画即可:

6e5c6591a45da35ee58ece32cf2f5cbb.jpeg

选择这个圆柱,将它的材质更换为刚刚创建的瞄准镜材质:

29350a966d7ff9e3f77492985be0aa59.jpeg

接下来在瞄准组件中添加一个摄像机,并且调整好位置,拍摄倍率直接修改 Fov 数值即可:

3429e5c5b0f6de25ea2980fa4d868a11.jpeg

根据摄像机预览画面,调整好数值以后,往下拉到最下面,在 RenderToTexture 中,将之前建立的名为「瞄准镜」的可渲染纹理,拖进其中:

a0016741c4f925a8923f5ed9ff838c92.jpeg

如果一切顺利,我们可能直接就在编辑器中看到效果。现在运行一下(我这里使用了自定义脚本,让瞄准的动作看起来更加准确),你可以看到在瞄准镜中已经有了放大的画面,我们再走动一下瞄准不同的地方试试:

d7c526d6a0b5adc7f61c29ce31d56c94.gif

到此为止,瞄准镜的实现就已经搞定了,是不是很简单呢,下面我们试一下监视器效果。

监视器

在很多游戏中,玩家可以通过监视器屏幕看到摄像头传来的数据,这类效果同样也是使用 RenderToTexture 实现。本次我们将做一个无人机控制板+一个街头的摄像机

同样也是使用前面搭建的街景,在这个房间场景中,我们用一个框框来表示无人机控制板;而监视器的画面则直接投射到电视中。完成了这两个后,将空间中的面片放置到准确位置,并且放置一个摄像机观察场景:

7fb4ea1d08cf065f262cd83faddb4fed.jpeg

b54a2523b68597af82e6a0661579bf65.png

新建一个可渲染纹理,命名为「无人机」,同样建立一个材质,着色器选内置-unlit,然后选择 UseTexture,勾选 RT,下面选择对应的可渲染纹理。这里我们就不新建监视器的渲染纹理和材质了,直接使用之前瞄准镜的即可:

ccdd754d282cb9f55accb834c63c3d87.png

现在分别建立两个摄像机,为方便观察将无人机简单做成一个小飞机的样子:

0dfe458955c47aad0413e9db7e9ee1ac.png

然后将街头摄像机摆好俯视即可,适当地作一些脚本完成控制,这些脚本如下:

first-person-camera.ts 来自官方例子工程:

import { _decorator, Component, math, systemEvent, SystemEvent, KeyCode, game, cclegacy, Touch, EventKeyboard, EventMouse } from "cc";
const { ccclass, property, menu } = _decorator;
const v2_1 = new math.Vec2();
const v2_2 = new math.Vec2();
const v3_1 = new math.Vec3();
const qt_1 = new math.Quat();
const id_forward = new math.Vec3(0, 0, 1);
const KEYCODE = {
 W: 'W'.charCodeAt(0),
 S: 'S'.charCodeAt(0),
 A: 'A'.charCodeAt(0),
 D: 'D'.charCodeAt(0),
 Q: 'Q'.charCodeAt(0),
 E: 'E'.charCodeAt(0),
 w: 'w'.charCodeAt(0),
 s: 's'.charCodeAt(0),
 a: 'a'.charCodeAt(0),
 d: 'd'.charCodeAt(0),
 q: 'q'.charCodeAt(0),
 e: 'e'.charCodeAt(0),
 SHIFT: KeyCode.SHIFT_LEFT ,
};

@ccclass("COMMON.FirstPersonCamera")
@menu("common/FirstPersonCamera")
export class FirstPersonCamera extends Component {

 @property
 moveSpeed = 1;

 @property
 moveSpeedShiftScale = 5;

 @property({ slide: true, range: [0.05, 0.5, 0.01] })
 damp = 0.2;

 @property
 rotateSpeed = 1;

 _euler = new math.Vec3();
 _velocity = new math.Vec3();
 _position = new math.Vec3();
 _speedScale = 1;

 onLoad() {
  math.Vec3.copy(this._euler, this.node.eulerAngles);
  math.Vec3.copy(this._position, this.node.position);
 }

 onDestroy() {
  this._removeEvents();
 }

 onEnable() {
  this._addEvents();
 }

 onDisable() {
  this._removeEvents();
 }
 update(dt: number) {
  // position
  math.Vec3.transformQuat(v3_1, this._velocity, this.node.rotation);
  math.Vec3.scaleAndAdd(this._position, this._position, v3_1, this.moveSpeed * this._speedScale);
  math.Vec3.lerp(v3_1, this.node.position, this._position, dt / this.damp);
  this.node.setPosition(v3_1);
  // rotation
  math.Quat.fromEuler(qt_1, this._euler.x, this._euler.y, this._euler.z);
  math.Quat.slerp(qt_1, this.node.rotation, qt_1, dt / this.damp);
  this.node.setRotation(qt_1);
 }

 private _addEvents() {
  systemEvent.on(SystemEvent.EventType.MOUSE_WHEEL, this.onMouseWheel, this);
  systemEvent.on(SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
  systemEvent.on(SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
  systemEvent.on(SystemEvent.EventType.TOUCH_MOVE, this.onTouchMove, this);
  systemEvent.on(SystemEvent.EventType.TOUCH_END, this.onTouchEnd, this);
 }

 private _removeEvents() {
  systemEvent.off(SystemEvent.EventType.MOUSE_WHEEL, this.onMouseWheel, this);
  systemEvent.off(SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
  systemEvent.off(SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
  systemEvent.off(SystemEvent.EventType.TOUCH_MOVE, this.onTouchMove, this);
  systemEvent.off(SystemEvent.EventType.TOUCH_END, this.onTouchEnd, this);
 }

 onMouseWheel(e: EventMouse) {
  const delta = -e.getScrollY() * this.moveSpeed / 24; // delta is positive when scroll down
  math.Vec3.transformQuat(v3_1, id_forward, this.node.rotation);
  math.Vec3.scaleAndAdd(v3_1, this.node.position, v3_1, delta);
  this.node.setPosition(v3_1);
 }

 onKeyDown(e: EventKeyboard) {
  const v = this._velocity;
  if (e.keyCode === KEYCODE.SHIFT) { this._speedScale = this.moveSpeedShiftScale; }
  else if (e.keyCode === KEYCODE.W || e.keyCode === KEYCODE.w) { if (v.z === 0) { v.z = -1; } }
  else if (e.keyCode === KEYCODE.S || e.keyCode === KEYCODE.s) { if (v.z === 0) { v.z = 1; } }
  else if (e.keyCode === KEYCODE.A || e.keyCode === KEYCODE.a) { if (v.x === 0) { v.x = -1; } }
  else if (e.keyCode === KEYCODE.D || e.keyCode === KEYCODE.d) { if (v.x === 0) { v.x = 1; } }
  else if (e.keyCode === KEYCODE.Q || e.keyCode === KEYCODE.q) { if (v.y === 0) { v.y = -1; } }
  else if (e.keyCode === KEYCODE.E || e.keyCode === KEYCODE.e) { if (v.y === 0) { v.y = 1; } }
 }

 onKeyUp(e: EventKeyboard) {
  const v = this._velocity;
  if (e.keyCode === KEYCODE.SHIFT) { this._speedScale = 1; }
  else if (e.keyCode === KEYCODE.W || e.keyCode === KEYCODE.w) { if (v.z < 0) { v.z = 0; } }
  else if (e.keyCode === KEYCODE.S || e.keyCode === KEYCODE.s) { if (v.z > 0) { v.z = 0; } }
  else if (e.keyCode === KEYCODE.A || e.keyCode === KEYCODE.a) { if (v.x < 0) { v.x = 0; } }
  else if (e.keyCode === KEYCODE.D || e.keyCode === KEYCODE.d) { if (v.x > 0) { v.x = 0; } }
  else if (e.keyCode === KEYCODE.Q || e.keyCode === KEYCODE.q) { if (v.y < 0) { v.y = 0; } }
  else if (e.keyCode === KEYCODE.E || e.keyCode === KEYCODE.e) { if (v.y > 0) { v.y = 0; } }
 }

 onTouchMove(e: Touch) {
  e.getStartLocation(v2_1);
  if (v2_1.x > cclegacy.winSize.width * 0.4) { // rotation
   e.getDelta(v2_2);
   this._euler.y -= v2_2.x * 0.5;
   this._euler.x += v2_2.y * 0.5;
  } else { // position
   e.getLocation(v2_2);
   math.Vec2.subtract(v2_2, v2_2, v2_1);
   this._velocity.x = v2_2.x * 0.01;
   this._velocity.z = -v2_2.y * 0.01;
  }
 }

 onTouchEnd(e: Touch) {
  e.getStartLocation(v2_1);
  if (v2_1.x < cclegacy.winSize.width * 0.4) { // position
   this._velocity.x = 0;
   this._velocity.z = 0;
  }
 }

 changeEnable() {
  this.enabled = !this.enabled;
 }
}

PlayerController.ts:

import { _decorator, Component, Node, KeyCode, EventKeyboard, RigidBody, Vec3, v3, input, Input } from 'cc';
const { ccclass, property } = _decorator;



@ccclass('PlayerController')
export class PlayerController extends Component {
    @property
    moveSpeed = 10;
    @property
    rotSpeed = 90;
    private keyMap = {};
    start() {
        
        input.on(Input.EventType.KEY_DOWN,this.onKeyDown,this);
        input.on(Input.EventType.KEY_UP,this.onKeyUp,this);
    }
    setRotSpeed(value){
        this.rotSpeed = value;
    }
    private onKeyDown(e: EventKeyboard) {
        this.keyMap[e.keyCode] = true;
    }
    private onKeyUp(e: EventKeyboard) {
        this.keyMap[e.keyCode] = false;
    }
    private vec3:Vec3 = v3();
    update(deltaTime: number) {        
        if (this.keyMap[KeyCode.KEY_W]) {
            Vec3.add(this.vec3,this.node.position,this.node.forward.clone().multiplyScalar(-this.moveSpeed * deltaTime));
            this.node.position = this.vec3; 
        } else if (this.keyMap[KeyCode.KEY_S]) {
            Vec3.add(this.vec3,this.node.position,this.node.forward.clone().multiplyScalar(this.moveSpeed * deltaTime));
            this.node.position = this.vec3; 
        }else {
        }
        if (this.keyMap[KeyCode.KEY_A]) {
            this.node.setRotationFromEuler(0,this.node.eulerAngles.y + deltaTime * this.rotSpeed,0);
        }else if (this.keyMap[KeyCode.KEY_D]) {
            this.node.setRotationFromEuler(0,this.node.eulerAngles.y + deltaTime * -this.rotSpeed,0);
        }
    }
}

FirstPersonGunCamreSc.ts:

import { _decorator, Component, Node, CCObject, Vec3, Quat, tween, Camera } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('FirstPersonGunCamreSc')
export class FirstPersonGunCamreSc extends Component {
    private original_position:Vec3;
    @property(Camera)
    aniCamera:Camera = null;
    start() {
        this.original_position = this.node.position.clone();
    }
    aim(){
        tween(this.node).to(0.3,{position:this.aniCamera.node.position}).start();
        tween(this.getComponent(Camera)).to(0.3,{fov:this.aniCamera.fov}).start();
    }
    unAim(){
        tween(this.node).to(0.3,{position:this.original_position}).start();
        tween(this.getComponent(Camera)).to(0.3,{fov:45}).start();
    }
    update(deltaTime: number) {
        
    }
}

GunSc.ts:

import { _decorator, Component, Node, SkeletalAnimation, input, Input, EventKeyboard, misc, KeyCode } from 'cc';
import { FirstPersonGunCamreSc } from './FirstPersonGunCamreSc';
import { PlayerController } from './PlayerController';
const { ccclass, property } = _decorator;

@ccclass('GunSc')
export class GunSc extends Component {

    @property(SkeletalAnimation)
    gunSA:SkeletalAnimation = null;
    @property(FirstPersonGunCamreSc)
    FirstPersonGunCam:FirstPersonGunCamreSc = null;
    
    start() {
        this.playIndex(5);
        input.on(Input.EventType.KEY_DOWN,this.onKeyDown,this);
    }
    private _isaim = false;
    private onKeyDown(e:EventKeyboard){
        if(e.keyCode == KeyCode.SPACE){
            this._isaim = !this._isaim;
            if(this._isaim){
                this.aim();
            }else{
                this.unAim();
            }
        }
    }
    
    update(deltaTime: number) {
        
    }
    private playIndex(index) {
        const animatname = this.gunSA.clips[index].name;
        this.gunSA.play(animatname);
        this.gunSA.crossFade(animatname);
        
    }
    aim(){
        this.FirstPersonGunCam.aim();
        this.playIndex(1);
        this.getComponent(PlayerController)?.setRotSpeed(30);
    }
    unAim(){
        this.playIndex(5);
        this.FirstPersonGunCam.unAim();
        this.getComponent(PlayerController)?.setRotSpeed(90);
    }
}

现在给摄像机上添加渲染纹理,为监视器添加对应的材质。如此一来,我们在电视上看到了街头监视器画面,而屏幕左边则投射了无人机画面。由于有控制脚本,我们可以控制它到处飞行一下,看看效果:

1ae9f75c3a41b2d3fdc7653fe12b23ef.gif

资源链接

  • 源码下载丨Cocos Store

https://store.cocos.com/app/detail/3803

  • 视频教程(UP 主:Nowpaper)

https://www.bilibili.com/video/BV1S34y1j7Ha

  • 论坛讨论帖:

https://forum.cocos.org/t/topic/136021

今天的文章就到这里,我是 Nowpaper,一个混迹游戏行业的老爸,如果您喜欢我的分享,不妨多多点赞留言,也欢迎关注我的 B 站,您的支持是我更新的动力,下次再见!

Nowpaper 往期分享

《笼中窥梦》多维空间视错觉效果

《守望先锋》同级的枪弹射击体验

《时空幻境》时间倒放玩法

《饥荒》同款视觉表现

用 RenderTexture 实现小地图与传送门


0ccda801f599c359b5c9d4e7e78e0473.jpeg

Cocos 开发者沙龙「深圳站」将于6月11日(下周六)举行。引擎技术总监 Panda、腾讯光子高级工程师子龙山人、深圳小爱灵动游戏内容负责人孙二喵、中青宝项目主美杨甜、TopOn 高级商业化经理温国能、Liftoff+Vungle Senior Account Manager Bruce蓝学渊将为大家带来干货分享。扫描上方二维码或点击文末【阅读原文】即刻报名吧!

往期精彩

30ac4c94de487aab5e99844c5aa17c58.png

1d5dfb20c5b77ae532f334d4f726ce75.png

5e8af2f2377290d506911b15eb62d395.png

ad8ab28e8a9ec119573bcb3ebcc53e70.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CocosCreator是一种基于 TypeScript的游戏开发引擎,而protobuf是一种数据序列化格式。要在CocosCreator使用protobuf实现登录功能,首先需要进行以下几个步骤: 1. 下载protobuf库:在项目的assets目录创建一个新的文件夹,例如"protobuf",然后从protobuf官方网站上下载protobuf的JavaScript库文件,并将其解压到该文件夹。 2. 创建.proto文件:在项目的assets目录创建一个新的文件,例如"login.proto",并在其定义登录功能所需的消息结构。例如,可以定义一个"LoginRequest"消息,包含用户名和密码字段,并定义一个"LoginResponse"消息,表示登录结果。 3. 生成JavaScript代码:打开终端,进入到protobuf库所在的文件夹,执行以下命令来生成JavaScript代码文件: protoc --js_out=import_style=commonjs,binary:生成代码路径 -I=proto文件所在路径 proto文件 这将根据.proto文件生成对应的JavaScript代码文件,用于在CocosCreator进行序列化和反序列化。 4. 在CocosCreator使用protobuf:将生成的JavaScript代码文件拷贝到CocosCreator项目的assets目录,然后在适当的地方引入protobuf库。 5. 编写登录功能代码:在需要实现登录功能的脚本文件使用引入的protobuf库来序列化登录请求数据,并将其发送到服务器,并处理服务器返回的响应数据。 总的来说,使用CocosCreator和protobuf实现登录功能需要先创建.proto文件,然后通过protobuf库生成对应的JavaScript代码文件,并将其引入项目,在代码使用protobuf库进行消息的序列化和反序列化,以实现与服务器的通信。这样就可以在CocosCreator使用protobuf来实现登录功能了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值