Cocos游戏开发基础


脚本与组件

创建一个Typescript脚本,默认的模板如下:

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

@property
private isDebug: boolean = false;

@ccclass('GameInit')
export class GameInit extends Component {
    start() {

    }

    update(deltaTime: number) {
        
    }
}
  • Cocos是基于组件化开发,游戏的运行是基于在节点上挂载的组件(脚本)来运行的。
  • 其中import导入了Cocos的模块,_decorator引入了装饰器,修饰类型或数据成员。Component是组件类的基类。Node是节点,是组件的载体。
  • ccclass是类装饰器,装饰之后编辑器的界面才能读取和显示该组件。property装饰类的数据成员,让编辑器可见。
  • onLoad是组件第一次加载到场景中调用,start是第一次画面刷新的时候调用的,update每次画面刷新调用一次。
  • 每个组件都有一个数据成员node,指向该节点(this.node)。常用 用法:this.node.parents,this.node.children[0],this.node.getChildByName(name),this.node.getChildByPath,this.node.name,this.node.getComponent,this.node.getComponents,this.node.getComponentsInChildren,this.node.addComponent。使用的时候可以省略中间的.node。

向量


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

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

    start () {
        // new + 构造函数;
        var v = new Vec3(); // (0, 0, 0)
        v = new Vec3(3, 4);  // (3, 4, 0)
        v = new Vec3(3, 4, 5); // (3, 4, 5)
        console.log(v);


        var vv = new Vec3(v); // v2(3, 4, 5)
        console.log(vv);
        // end

        vv = v3(7, 8 ,9);
        console.log(vv);

        console.log(vv.length());
        console.log(Vec3.len(vv));

        console.log(Vec3.UNIT_X, Vec3.FORWARD, Vec3.ONE, Vec3.NEG_ONE);
    }
}

平移、缩放、旋转

export class GameMgr extends Component {
 
    start () {
        // var ball = this.node.getChildByName("Sphere");
        // console.log(ball?.worldPosition, ball?.position);
        
        // get/set接口
        // console.log(ball?.worldScale, ball?.scale);   // x缩放多少,y缩放多少, z缩放多少
        // var pos: Vec3 = (ball?.getPosition() as Vec3); // 获取一个坐标
        // pos.x += 1.0;
        // ball?.setPosition(pos);

        // this.node.setWorldRotationFromEuler(0, 60, 0);
         }

    update(dt: number) {
        var pos = this.node.getPosition();
        pos.x += (2 * dt);
        this.node.setPosition(pos);


        var rot: Vec3 = this.node.eulerAngles;
        rot.y += (30 * dt);
        this.node.setRotationFromEuler(rot);

        
    }
}

系统事件


import { _decorator, Component, Node, SystemEvent, systemEvent, System, EventAcceleration, EventKeyboard, Touch, EventTouch, SystemEventType, macro } from 'cc';
const { ccclass, property } = _decorator;

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


    

    public start(): void { // 在初始化这里,我们就可以监听我们的事件;
        // 监听触摸事件
        systemEvent.on(SystemEventType.TOUCH_START, this.onTouchStart, this);
        systemEvent.on(SystemEventType.TOUCH_MOVE, this.onTouchMove, this);
        systemEvent.on(SystemEventType.TOUCH_END, this.onTouchEnd, this);
        systemEvent.on(SystemEventType.TOUCH_CANCEL, this.onTouchCancel, this);
        // end

        // 监听我们的键盘事件
        systemEvent.on(SystemEventType.KEY_DOWN, this.onKeyDown, this);
        systemEvent.on(SystemEventType.KEY_UP, this.onKeyUp, this);
        // end

        // 重力感应
        systemEvent.on(SystemEventType.DEVICEMOTION, this.onDeviceMotion, this);
        // end
    }

    onDestroy(): void {
        // 当我们开始监听的时候,一定要思考什么时候释放;
        systemEvent.off(SystemEventType.TOUCH_START);

        // systemEvent.on(SystemEventType.TOUCH_START, this.onTouchStart, this);
        systemEvent.off(SystemEventType.TOUCH_MOVE, this.onTouchMove, this);
        systemEvent.off(SystemEventType.TOUCH_END, this.onTouchEnd, this);
        systemEvent.off(SystemEventType.TOUCH_CANCEL, this.onTouchCancel, this);
        // end

        // 监听我们的键盘事件
        systemEvent.off(SystemEventType.KEY_DOWN, this.onKeyDown, this);
        systemEvent.off(SystemEventType.KEY_UP, this.onKeyUp, this);
        // end

        // 重力感应
        systemEvent.off(SystemEventType.DEVICEMOTION, this.onDeviceMotion, this);
        // end
    }

    private onDeviceMotion(event: EventAcceleration): void {

    }

    private onKeyDown(event: EventKeyboard): void {
        
        switch(event.keyCode) {
            case macro.KEY.s: // S
                console.log("S 建被按下了");
            break;
            case 87: // w
            break;
            case 65: // A
            break;
            case 68: // D
            break;

            case macro.KEY.space:
                console.log("空格按键按下了");
            break;
        }
    }

    private onKeyUp(event: EventKeyboard): void {
        
        switch(event.keyCode) {
            case 83: // S
            break;
            case 87: // w
            break;
            case 65: // A
            break;
            case 68: // D
            break;
        }
    }

    private onTouchStart(touch: Touch, event: EventTouch): void {
        // this --->target;
        console.log("onTouchStart", touch, touch.getLocation());
        
    }

    private onTouchEnd(touch: Touch, event: EventTouch): void {
        // this --->target;
        console.log("onTouchEnd");
    }

    private onTouchMove(touch: Touch, event: EventTouch): void {
        console.log("onTouchMove");
        console.log("getDelta", touch, touch.getDelta());
    }

    private onTouchCancel(touch: Touch, event: EventTouch): void {
        console.log("onTouchCancel");
    }
}

预制体生成


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

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

    @property(Prefab)
    // private cubePrefab: Prefab | null = null;
    private cubePrefab: Prefab = null as unknown as Prefab;
    
    start () {
        // 实例化, 一定要加到场景树才可以了;
        var cube: Node = instantiate(this.cubePrefab);
        // 节点.addChild: 往这个节点,加入一个孩子节点
        // this.node.addChild(cube); // API 就是将我们的节点加入到我们的场景树;
        this.node.parent?.addChild(cube);
        // end
    }

}

3D知识

骨骼动画

天空盒、雾

摄像机

透视投影:3D使用
正交投影:2D UI使用

内置shader

物理组件


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

@ccclass('CubeCtrl')
export class CubeCtrl extends Component {
    private body: RigidBody = null as unknown as RigidBody;
    // private body: RigidBody | null = null;

    onLoad(): void {
        // step1: 获取刚体组件实例
        this.body = this.node.getComponent(RigidBody) as RigidBody;
        this.body.useGravity = true;
        
    }

    start(): void {
        /*
        // step2: 我们来给一个线性速度
        this.body.setLinearVelocity(v3(-1, 0, 0));
        //  单位应该是弧度
        // 绕哪个轴,每秒旋转多少度;
        this.body.setAngularVelocity(v3(0, Math.PI * 2, 0));
        */

        this.body.applyForce(v3(-1000, 0, 0));
    }
}

监听碰撞事件


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

@ccclass('EventCollision')
export class EventCollision extends Component {
    start(): void {
        // 获取任意类型的碰撞器实例,你就可以使用基类
        var collider = this.node.getComponent(Collider);
        collider?.on("onCollisionEnter", this.onCollisionEnter, this);
        collider?.on("onCollisionStay", this.onCollisionEnter, this);
        collider?.on("onCollisionExit", this.onCollisionExit, this);

        collider?.on("onTriggerEnter", this.onTriggerEnter, this);
        collider?.on("onTriggerStay", this.onTriggerStay, this);
        collider?.on("onTriggerExit", this.onTriggerExit, this);
    }
    
    private onTriggerEnter(event?: ICollisionEvent): void {
        console.log("onTriggerEnter", event);
    }

    private onTriggerStay(event?: ICollisionEvent): void {
        console.log("onTriggerStay");
    }

    private onTriggerExit(event?: ICollisionEvent): void {
        console.log("onTriggerExit");
    }

    private onCollisionEnter(event?: ICollisionEvent): void {
        console.log("onCollisionEnter");
    }

    private onCollisionStay(event?: ICollisionEvent): void {
        console.log("onCollisionStay");
    }

    private onCollisionExit(event?: ICollisionEvent): void {
        console.log("onCollisionExit");
    }
}

射线检测:


import { _decorator, Component, Node, PhysicsSystem, geometry, systemEvent, SystemEvent, Camera } from 'cc';
const { ccclass, property } = _decorator;

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

    @property(Camera)
    private mainCamera: Camera | null = null;

    start() {
        /*
        // 物理系统 PhysicsSystem类, 大写字幕
        // PhysicsSystem是一个全局单例 PhysicsSystem.instance
        // var r: geometry.Ray | null = null; 
        // step1: 创建一条射线
        var r: geometry.Ray = null as unknown as geometry.Ray;
        r = geometry.Ray.create(0, 0, 0, 0, 0, -1);

        // step2: 往3D世界里面放物体+物理碰撞器(编辑器,代码new 预制体)

        // step3: 调用接口来计算,到底我们的设想穿越了哪些物体
        // 两种模式(碰到一个物体以后,就停止检测(一个),继续检测,直到所有都做完)
        // if(PhysicsSystem.instance.raycastClosest(r, (1 << 0) | (1 << 1))) { // 碰到了物体
        if(PhysicsSystem.instance.raycast(r, (1 << 0) | (1 << 1))) { // 碰到了物体
            // step4: 拿结果;
            // console.log(PhysicsSystem.instance.raycastClosestResult);
            // console.log(PhysicsSystem.instance.raycastClosestResult.collider.node.name);
            console.log(PhysicsSystem.instance.raycastResults); 
        }
        else { // 没有碰到物体
            console.log("没有撞到任何物体");
        }
        */

        //  监听触摸事件;
        systemEvent.on(SystemEvent.EventType.TOUCH_START, this.onTouchStart, this);
    }

    private onTouchStart(e: any): void {
        // step1: 获取屏幕位置
        var screenPos = e.getLocation(); 
        // step2: 获取摄像机组件实例
        this.mainCamera = this.node.getComponent(Camera);

        // step3: 利用Camera,帮我们生成一个Ray对象
        // var ray: geometry.Ray = geometry.Ray.create();
        var ray: geometry.Ray = this.mainCamera?.screenPointToRay(screenPos.x, screenPos.y) as geometry.Ray;

        // step4: 射线检测,拿结果
        if(PhysicsSystem.instance.raycastClosest(ray)) {
            console.log(PhysicsSystem.instance.raycastClosestResult.collider.node.name);
        }
    }
}




UI组件

图片Sprite

2D图片资源,修改图片通过UI SpriteFrame。注意图片的类型要改为2D UI。
type:使用类型
Alpha混合:可以设置图片的和背景混合,半透明效果
Color:设置图片的颜色叠加
SpriteAtlas:图集
spriteFrame:图片
Trim:去除图片周围的透明区域
GrayScale:是否置灰
SizeMode:是否由用户指定大小
模式:简单模式,切片模式,平铺模式,填充模式

图集的使用:要包含图集本身和说明位置的.plist文件,这样引擎就能自动分割图集

字体Label

string:文本的内容
对齐方式:左对齐,中间对齐,右对齐
字体大小,系统字库FontFamily,font(自带字库,位图字体),是否使用系统字库
Clamp模式:以节点范围为大小
Shark:根据节点的大小,自动调整文字
Resize_height:根据宽度换行
None:以文字大小为节点大小
CacheMode:缓存模式,用于节约DrawCall

按钮Button

Transition:设置按下大小变动
设定对应的响应事件为对应的函数。

public onButtonClick(): void {
    console.log("onButtonClick");
}

UI的响应事件


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

@ccclass('EventListener')
export class EventListener extends Component {
 
    start () {
        this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
    }

    private onTouchStart(): void {
        console.log("onTouchStart");
    }
}

Mask组件

裁剪不需要显示的区域,例如小地图的应用,只能裁剪子节点
形状:矩形,扇形,图片(小于某个透明度被裁掉),多边形
反转:内部不显示还是外部不显示

ScollView

滚动列表
内容排版:加layout组件
滚动事件:滚动到最上方,最下方,最左方,最右方

屏幕适配

背景适配:背景的长度和宽度能够覆盖所有的屏幕 。

UI适配:加上widget组件,把UI分为上下左右中五个部分,设置左边部分的UI锚点为父物体左边,父物体固定为左边并且上下拉伸,其他同理

需要适配水滴屏

本地存储

原理:因为只能存储字符串,因此表序列化成Json对象来进行存储,使用时反序列化出来即可

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

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


    start () {
        var loginData = {uname: "blake", upwd: "123"};
        var loginStr = JSON.stringify(loginData);


        // 添加一个存储, key, value
        localStorage.setItem("loginData", loginStr);

        // 获取一个key
        var strValue = localStorage.getItem("loginData");
        var data = JSON.parse(strValue);
        console.log(data);

        // 删除一个key
        localStorage.removeItem("loginData");
    }
}

定时器

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

@ccclass('GameApp')
export class GameApp extends Component {
    
    start () {
        // 触发一次,3秒以后的定时器;
        this.scheduleOnce(()=>{
            // this --->外面当前的this;
            console.log("5秒时间到了!");
            var item = find("Canvas/Item");

            // 移除场景中的一个节点
            // item?.removeFromParent();
            // end

            // 移除掉场景中的一个节点,并且销毁节点;
            // item?.destroy();
            // 删除掉一个组件实例类型就是Sprite组件实例
            item?.getComponent(Sprite)?.destroy();

        }, 5);


        // 5秒以后启动定时器,每隔2秒,调用一次回调函数,一共调用6次;
        var func = ()=>{
            // this --->外面的this;
            console.log("定时器触发了,2秒一次");
        };

        this.schedule(func, 2, macro.REPEAT_FOREVER, 5);

        // 取消定时器:
        this.scheduleOnce(()=>{
            // 取消掉所有的组件实例的定时器
            // this.unscheduleAllCallbacks();

            // 取消特定的定时器? schedule是哪个函数,你现在就要传哪个函数进去;
            this.unschedule(func);
        }, 20)

        
        console.log(this.node.name, this.node.layer);  // (1 << 0) (1<<1) (1 << 2)
        
        
    }
}

基于AB包加载资源

每个文件夹都有的功能,可以配置为AssetBundle,名字为文件夹本身的名字

默认有两个AB包:

  1. 场景中存在的资源会被打入main ab包
  2. resources文件夹中的文件会被打入另一个ab包
  3. 现在的项目尽量不使用resource文件夹,而是新建一个AssetPackage文件夹,存放ab资源包
    
    import { _decorator, Component, Node, macro, find, Sprite, assetManager, AssetManager, SpriteAtlas, Texture2D, AudioClip, AudioSource, SpriteFrame } from 'cc';
    const { ccclass, property } = _decorator;
    
    @ccclass('GameApp')
    export class GameApp extends Component {
        
        start () {
            var itemSprite: Sprite = find("Canvas/Item")?.getComponent(Sprite) as Sprite;
            // 异步加载,不是马上就加载好,加载好会掉你的函数;
            assetManager.loadBundle("GUI", (err, ab: AssetManager.Bundle)=> {
                // 在这里才正式加载好;
                
                // 加载图集资源
                // 路径:是从我们的资源包的路径下开始
                ab.load("test/airplane", SpriteAtlas, (err, atlas: SpriteAtlas)=>{
                    if(err) {
                        console.log(err);
                        return;
                    }
    
                    // console.log(atlas);
                    var sp: SpriteFrame = atlas.getSpriteFrame("Jishen_W0") as SpriteFrame;
                    itemSprite.spriteFrame = sp;
                    sp.addRef();  
                });
                // end
    
                // 加载我们的声音文件
                ab.load("CK_attack1", AudioClip, (err, clip:AudioClip)=>{
                    if(err) {
                        console.log(err);
                        return;
                    }
    
                    var as: AudioSource = this.getComponent(AudioSource) as AudioSource;
                    as.clip = clip;
                    clip.addRef(); // 引用计数来判断,这个资源是否在使用;
                    as.loop = true;
                    as.play();
    
                    
                });
                // end
                
                // 释放ab包, 不会释放从ab包里面加载的资源;
                // assetManager.removeBundle(ab);
                // end
            });
    
            this.scheduleOnce(()=>{
                // var ab = assetManager.getBundle("GUI");
                // ab?.releaseAll(); // 释放所有资源
                // ab?.releaseUnusedAssets(); // 释放没有用的资源, 引用计数
    
                /*
                var ab = assetManager.getBundle("GUI");
                ab?.release("test/airplane");  // 基于ab包释放单个资源
                ab?.release("CK_attack1");
                assetManager.removeBundle(ab as AssetManager.Bundle);
                */
                var ab: AssetManager.Bundle | null = assetManager.getBundle("GUI");
                assetManager.removeBundle(ab as AssetManager.Bundle);
    
                // assetManager.releaseAsset();
                // assetManager.releaseUnusedAssets();
                // assetManager.releaseAll();
    
                // assetManager.loadRemote();
            }, 10);
        }
    }

Tween缓动对象

基于node上的脚本进行控制


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


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

    onLoad(): void {
        // step1: 确定你要欢动的target--->当前节点;  this.node;
        // step2: 创建一个我们的tween对象;
        var t = tween(this.node);
        // step3: 给欢动对象来下控制的命令;
        t.by(1, {position: new Vec3(200, 0, 0), scale: new Vec3(-1, -1, -1)});
        t.call(()=>{
            console.log("action end!!!");
        }); 

        // step4: 启动一个tween对象,让他控制我们的target来做上面的动作
        t.start();
    }
}

Json的序列化和反序列化


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


 
@ccclass('GameApp')
export class GameApp extends Component {
    onLoad(): void {
        var loginData = {
            uname: "blake",
            upwd: "123456",
        };

        var loginDataStr = JSON.stringify(loginData);
        console.log(loginData);
        console.log(loginDataStr);

        var data = JSON.parse(loginDataStr);
        console.log(data);
        console.log(data.uname, data.upwd);

    }
}

2D骨骼动画

  1. 首先需要有一张图集
  2. atlas文件表述了骨骼在图集中的位置
  3. json文件描述了动画控制
  4. 这三种文件要同名,存放一起

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

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

    start () {
        // step1: 获取组件实例, 哪个节点上的哪个组件实例; this.node/find;
        var animCom: sp.Skeleton = this.node.getComponent(sp.Skeleton);

        // step2: 调用接口来播放某个动画;
        animCom.animation = "animation_3_stop";
    }

}

如何制作摇杆

  1. 需要两张圆形图片
  2. 编写控制内部圆形的代码
    
    import { _decorator, Component, Node, Vec2, v2, v3, UITransform, Camera } from 'cc';
    const { ccclass, property } = _decorator;
    
     
    @ccclass('Joystick')
    export class Joystick extends Component {
    
        @property(Node)
        private stick: Node = null;
    
        public dir: Vec2 = v2(0, 0);
    
        @property
        private maxR: number = 100;
    
        @property(Camera)
        private uiCamera: Camera = null;
    
        private transfrom: UITransform = null;
    
        public onLoad(): void {
            // this.stick = this.node.getChildByName("stick");
            this.transfrom = this.node.getComponent(UITransform);
    
            this.stick.on(Node.EventType.TOUCH_START, this.OnTouchStart, this);
            this.stick.on(Node.EventType.TOUCH_CANCEL, this.OnTouchEnd, this);
            this.stick.on(Node.EventType.TOUCH_MOVE, this.OnTouchMove, this);
            this.stick.on(Node.EventType.TOUCH_END, this.OnTouchEnd, this);
        }
    
        private OnTouchStart(e): void {
            this.dir.x = this.dir.y = 0;
        }
    
        private OnTouchEnd(e): void {
            this.dir.x = this.dir.y = 0;
            this.stick.setPosition(0, 0);
        }
    
        private OnTouchMove(e): void {
            var screenPos: Vec2 = e.getLocation();
            // 摄像机,把我们的世界坐标,转到我们的屏幕坐标;
            var wPos = this.uiCamera.screenToWorld(v3(screenPos.x, screenPos.y, 0));
            var pos = this.transfrom.convertToNodeSpaceAR(wPos);
            pos.z = 0;
    
            var len = pos.length();
            if(len > this.maxR) {
                pos.x = pos.x * this.maxR / len;
                pos.y = pos.y * this.maxR / len;
                len = this.maxR;
            }
    
            this.dir.x = pos.x / len; // cos(角度)
            this.dir.y = pos.y / len; // sin(角度)
    
            this.stick.setPosition(pos);
        }
    }

Box2D物理引擎

  • Box2D必须要有2D刚体
  • 在onLoad里面开启物理引擎
  • 地面设置静态刚体,人物等设置动态
  • 开启碰撞检测与碰撞分组

管理类:

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


@ccclass('PhysicMgr')
export class PhysicMgr extends Component {
    
    @property
    private isDebug: boolean = false;
    
    onLoad(): void {
        PhysicsSystem2D.instance.enable = true; // 开启物理引擎;

        if(this.isDebug) { // debugDraw;
            PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.All;
        }
        else {
            PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.None;
        }
    }
}
import { _decorator, Component, Node, BoxCollider2D, Contact2DType } from 'cc';
const { ccclass, property } = _decorator;
 
@ccclass('ItemContact')
export class ItemContact extends Component {

    private colliderBox: BoxCollider2D = null;

    onLoad(): void {
        // 获得你要监听碰撞事件得物理形状
        this.colliderBox = this.node.getComponent(BoxCollider2D);

        // 监听碰撞事件
        this.colliderBox.on(Contact2DType.BEGIN_CONTACT, this.OnBeginContant, this);
    }

    private OnBeginContant(self, other): void {
        console.log("OnBeginContant", other);
    }
}

微信小游戏分包加载

internal:引擎内部的资源
main:场景中的资源和依赖,没有指定ab包的
普通ab包:游戏引擎代码库cocos-js,引擎核心代码cc.js
自己写的代码在main ab包的index.js

注意:需要在cocos打包发布的界面中选择小游戏分包,引擎会自动把AssetBundle打成分包。引擎代码库会打成不超过4M的主包。

当总体积超过20M就需要做第三方资源部署。


文章作者: 微笑紫瞳星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 微笑紫瞳星 !
  目录