瞄准开镜!实现瞄准镜和监视器,可渲染纹理的常规玩法

如何在Creator3中实现瞄准镜和监视器

本文是B站视频的文字版,原版视频如下:

前言

Hello大家好,我是Nowpaper,一爸学游戏,越来越有趣

本文章所有素材在Cocos官方商店提供,有需要请自取

Cocos商店:瞄准镜工程项目

RenderToTexture这个技术,是有趣的技术,它能够将一个摄像机画面,渲染成纹理,然后和材质结合,让某一个Mesh显示成指定的画面,这方面的实际非常广泛,比如游戏中的镜子、监视器画面、瞄准镜、传送门等,甚至更进一步的,用户界面显示、动态纹理喷涂等,都可以借助RenderToTexture实现


image
image

Creator自从3.4以后的版本,这个技术已经相对完善,使用相关的功能,也不必像之前那样那么麻烦,在我之前传送门视频中,传送门中的画面就是使用这个技术实现,当时,还得写一些代码,而现在只需要在CreatorEditor中编辑,就可以轻松使用可渲染纹理了

准备

首先建立一个Creator项目,在这里我将使用最新的3.5版本,建立之后,使用一些素材搭建游戏场景,这个场景就是简单的街道,包含一些物体,让场景看起来稍微不那么单调

瞄准镜

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

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

Snipaste_2022-05-24_16-45-57

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

这个地方必须吐槽一下官方,由于在场景中骨骼动画不能预览,因此一些需要和动画一起的配合,就只能在实际运行中调整了,希望以后能够提供切换,哪怕像2D的龙骨动画,切换动画名就会变化为第一帧,好歹有个参考就行

现在新建一个可渲染纹理

image

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

现在在创建一个材质

image

着色器选择为内置-unlit

image

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

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

image

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

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

选择这个圆柱将它的材质,更换为刚刚创建的瞄准镜材质,它现在是黑色的别着急,下一步在瞄准组件中,添加一个摄像机

并且调整好位置,拍摄倍率直接修改Fov数值即可

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

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

动画9

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

监视器

在很多游戏中,玩家可以通过监视器屏幕,看到摄像头传来的数据,它使用的方案同样,也是RenderToTexture,在这个部分,我们将实现一个无人机控制板,一个街头的摄像机,同样也是使用前面搭建的街景,在这个房间场景中,弄一个无人机的面板,它是一个框框来表示,而监视器的画面直接投射到电视中,完成了两个之后,在空间中的面片放置到准确位置,并且放置一个摄像机观察场景

image

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

现在分别建立两个摄像机,无人机为了能够看到,简单作一个小飞机的样子

image

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

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);
    }
}

现在把摄像机上添加渲染纹理,为监视器添加对应的材质,运行一下看看效果

动画8

在电视上可以看到街头监视器画面,而屏幕左边的投射了无人机画面,由于有控制脚本,我们可以控制它到处飞行一下,看看效果

到此为止,我们用RenderToTexture,实现了几个有趣的玩法,如果你的创意更好,比如实现《笼中窥梦》,也是完全可行,我的另外一个视频,已经完整的讲述了有关细节,有兴趣可以去看看

结语

今天就到这里了,我是Nowpaper,一个混迹游戏行业的老爸,如果您喜欢我的视频,不妨三连一下,您的支持就是我更新的动力,那么我们下次再见

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

多维空间视错实现《笼中窥梦》,可渲染纹理和自定义着色器结合实现视觉方盒

游戏中射击的最酷实现,Creator3如何作出《守望先锋》同级的枪弹射击体验

时间倒放的有趣实现,在Creator中作物理回溯,开发《时空幻境》一样的倒退玩法

摄像机视角的有趣玩法,实现《饥荒》同款视觉表现,一毛一样

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

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

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

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

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

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

8赞

厉害,瞄准镜功能学到了!!!

赞赞赞 :+1::+1::+1::+1:

厉害啊.
顶.

杨老师越来越牛逼了

杨老师等等我,我跟不上了

这个开镜之后的延迟,咋整比较好啊