IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> UE4 资源的加载和创建对象 -> 正文阅读

[游戏开发]UE4 资源的加载和创建对象

参考

推荐:UE4的资源管理

UE4资源加载的几种方式

UE4静态/动态加载资源方式

Aery的UE4 C++游戏开发之旅(4)加载资源&创建对象

自定义蓝图库用于读取图片:【UE4插件】Ue4简单插件写法教学

疑问

UE中资源是什么?

https://docs.unrealengine.com/4.26/zh-CN/Basics/AssetsAndPackages/
在UE4中,项目中的所有资源文件,可将其看作序列化到文件中的 UObject

UE中资源文件和内存里对象的关系

来自
UE4的资源管理

UE4的资源,就是在工程文件夹下的那些非代码文件,比如Content下面的网格,材质,蓝图等这些文件,大部分资源是以uasset作为后缀的,也有其他后缀如地图关卡的umap等。

在打包时,这些文件可能会根据平台需要,被cook成更小的平台专用文件,然后被放在后缀是pak的压缩包里。游戏运行时,程序就会挂载解压这些pak包,然后加载包中的资源文件来使用。打个不是很合适的比方,Pak包就类似于Unity的AssetBundle包,uasset文件就类似于unity的.meta管理的那些资源文件。

程序在用资源的时候,并不是直接在用这些文件,而是要把这些文件转化为UObject或其他程序可以用的内存对象。 比如网格资源文件,程序用的实际是UStaticMesh对象。而把资源文件转变为内存里的UObject对象,就是资源管理做的事情。

对于UE4来说,这个过程大概有这几个步骤:

  1. 读取资源文件的数据到内存
  2. 根据内存的二进制数据,把空壳对象反序列化成实际的对象
  3. 如果这个对象有依赖其他对象,就递归的去做1和2的操作,直到这个对象完整可用
  4. 调用对象的初始化函数,并将对象加入到引擎的对象管理中

UE4是通过路径来关联索引资源的,每个资源都有他的唯一路径。
示例
在这里插入图片描述

  • 红色部分:(StaticMesh)
    是资源的类型,平时加载资源的时候也可以不要,加载函数内部会截掉这部分只留单引号里面的路径,所以其实前面这个StaticMesh或Blueprint可以不写,在编辑器Content Browser里右键点资源拷路径是有这部分的,我猜引擎留着这部分可能是为了用户清楚他的类型是什么,更方便识别一些,这个只是给人看的,程序不看这个。

  • 绿色部分:(/Game)
    是资源的分区。大部分资源的路径都是以/Game开头,这个其实表示这个资源是在游戏的Content目录下面,也可以是/Engine就表示引擎下面的,比如下图,或者对应插件目录

  • 蓝色部分:(/Geometry/Meshes)
    就跟操作系统文件管理器中的一样,表示资源在哪个文件夹下面

  • 黄色部分:(1M_Cube)
    资源的包(Package)名,也就是这个资源所在的真实物理资源文件(uasset/umap)的名字,包其实就是UE4将对象按照自己的规则序列化到磁盘上的文件,在Content Browser里看到的每一个文件都是一个包

  • 紫色部分:(1M_Cube)
    资源的对象名,因为物理的资源文件里面可能有多个对象,这个名字可以唯一标识包的内部每个对象的唯一名字。比如蓝图资源里有多个UObject,一个关卡文件里有多个Actor,一个UI蓝图里有多个控件。如果不写,UE4的某些接口会默认以包名补充到后面,也就是说默认使用和包名相同的对象名,但有的接口又可能不做处理,所以还是建议写。如果用的不是默认对象,而是资源对象的类,就要在后面加一个_C,如果是CDO对象,就要在前面加Default__

  • 冒号后面的部分
    有些资源路径后面会带冒号:接一个文件名,这种其实是对象的子对象名,有的资源对象内部有子对象,比如C++类里的子类,这个不常用,知道即可

加载资源和创建对象的区别?

加载资源实际上是在做反序列化,从而在内存中构造出对象

创建对象就是调用类的构造函数从而在内存中构造出对象

资源加载和创建对象

在UE4中,项目中的所有资源文件,可将其看作序列化到文件中的 UObject,加载资源实际上是在做反序列化,从而在内存中构造出对象

资源的硬引用和软引用

参考:
【UE4 C++ 基础知识】<10>资源的引用

资源硬引用:
即对象 A 引用对象 B,并导致对象 B 在对象 A 加载时加载。通俗点说,硬引用所表示的资源在引用初始化时就加载进内存,因此硬引用的资源几乎不需要加载方法。

资源软引用:
软性引用,即对象 A 通过间接机制(例如字符串形式的对象路径)来引用对象 B。

硬引用的问题是在容易一开始就加载全部硬引用表示的资源,这可能导致资源载入时间过长。而软引用则是可随时灵活加载资源的一种引用,而不用硬性地一开始就加载。

UE中主要涉及到 FSoftObjectPath、FSoftClassPath、TSoftObjectPtrr< T >,TSoftClassPtr< T >,使用这些方式时,需要手动加载资源(同步/异步加载:LoadObject, StaticLoadObject,FStreamingManager)

FSoftObjectPath

FSoftObjectPath是一个简单的结构体,其中有一个字符串包含资源的完整名称。它实质就是用一个字符串来表示对应的资源,从而可以随时通过字符串找到硬盘上的目标资源,将其载入进内存。

FSoftObjectPath.SolveObject() :可以检查其引用的资源是否已经载入在内存中,若载入则返还资源对象指针,否则返还空。
FSoftObjectPath.Reset() :重置软引用为空

AllowedClasses meta标签可以筛选资源类型

示例
// .h 文件
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoftObject", meta = (AllowedClasses = "SkeletalMesh, StaticMesh" ))
	FSoftObjectPath SoftObjectPath1;

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoftObject", meta = (AllowedClasses = "Texture2D"))
	FSoftObjectPath SoftObjectPath2;

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoftObject", meta = (AllowedClasses = "Blueprint Class"))
	FSoftObjectPath SoftObjectPath3;

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoftObject", meta = (AllowedClasses = "Drone")) //自定义类型 不推荐
	FSoftObjectPath SoftObjectPath4;
// .cpp文件
void ADrone::BeginPlay()
{
	Super::BeginPlay();

	if (SoftObjectPath1.IsValid()){ /* 处理*/ }
	if (SoftObjectPath2.IsNull()){ /* 处理*/ }
	if (SoftObjectPath3.IsAsset()){ /* 处理*/ }
	FString SoftObjectPath4_AssetName = SoftObjectPath4.GetAssetName();			
	FString SoftObjectPath3_AssetPath = SoftObjectPath3.GetAssetPathString();
}

FStringAssetReference:其实只是一个听起来更容易理解的别名,它实际在UE4源码里是这样的:

typedef FSoftObjectPath FStringAssetReference;

FSoftClassPath
FSoftClassPath 继承自 FSoftObjectPath,用于存储一个类型的软引用,用法与 FSoftObjectPath 差不多
MetaClass meta标签可以筛选类类型

// .h 文件
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoftObjectClass")
	FSoftClassPath SoftClassPath;

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoftObjectClass", meta = ( MetaClass= "Pawn"))
	FSoftClassPath SoftClassPath_Pawn;

TSoftObjectPtr< T >:
TSoftObjectPtr是包含了FSoftObjectPath的TWeakObjectPtr,是智能指针的一种,可通过模板参数来设置特定资源类型,这样就可以限制编辑器UI仅允许选择特定的资源种类。

TSoftObjectPtr 与蓝图中的 SoftObjectReference 是一回事

可用于在异步加载完成触发回调函数时,获取资源对应的对象指针

TSoftObjectPtr.IsPending() :方法可检查资源是否已经加载到内存中,未加载到内存,然后true;已经加载,返回false

TSoftObjectPtr.Get() :如果其引用的资源是否已经载入在内存中,若已载入则返还资源对象指针,
否则返还空。
想要资源加载进内存,则可以调用ToSoftObjectPath()来得到FSoftObjectPaths用于加载。

TSoftClassPtr< T >
获取类的软引用,转成 UClass*

示例
// .h
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoftClassPtr")
	TSoftClassPtr<AActor> SoftClassPtr_Actor;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoftClassPtr")
	TSoftClassPtr<UUserWidget> SoftClassPtr_UserWidget;
// .cpp 

if (SoftClassPtr_Actor.IsPending())
{
	UClass* MyActor = SoftClassPtr_Actor.Get();
}

加载资源的几种方式

参考:
引用资源

UE4 异步资源加载—引用

关于资源加载,个人觉得应该有如下三类需求:

  • 资源在游戏一开始启动的时候,就加载到内存中
  • 资源应该可以通过给定路径,在需要的时候按需同步加载
  • 资源应该可以通过给定路径,在需要的时候按需异步加载

下面是UE中引用资源的一些方法

  1. 直接属性引用

先在头文件中定义该资源类型的指针,并将其设置为UPROPERTY(EditDefaultsOnly),然后在蓝图中将此变量赋予资源,最后就可以直接引用该变量设置资源了。

/** construction start sound stinger */
UPROPERTY(EditDefaultsOnly, Category=Mesh)
UStaticMesh* MeshData;

这种方式点击Play按钮就全部会默认都加载。

  1. 构造时加载
#include "UObject/ConstructorHelpers.h" //需要include的头文件

FObjectFinder<T> / FClassFinder<T>

注意:

  • 只能在类的构造函数中使用,如果试图自己创建一个方法并且在这个方法中使用FObjectFinder / FClassFinder的方式去引用资源,会引起整个编译器的crash。(
  • 其次,FObjectFinder/FClassFinder变量必须是static的,从而保证只有一份资源实例

FObjectFinder< T >:
一般用来加载非蓝图资源,比如StaticMesh、Material、SoundWave、ParticlesSystem、AnimSequence、SkeletalMesh等资源:

//加载材质示例
static ConstructorHelpers::FObjectFinder<UMaterialInterface> WoodMaterial(TEXT("Material'/Game/StarterContent/Materials/M_Wood_Walnut.M_Wood_Walnut'"));
if (WoodMaterial.Object)
{
    GetMesh()->SetMaterial(0, WoodMaterial.Object);
}

//加载纹理示例
static ConstructorHelpers::FObjectFinder<UTexture2D> ObjectFinder(TEXT("Texture2D'/Game/Textures/tex1.tex1'"));
UTexture2D* Texture2D = ObjectFinder.Object;

FClassFinder< T >:
一般用来加载蓝图资源并获取蓝图Class。这是因为如果C++要用蓝图创建对象,必须先获取蓝图的Class,然后再通过Class生成蓝图对象:

static ConstructorHelpers::FClassFinder<AActor> BPClassFinder(TEXT("/Game/Blueprints/MyBP"));
TSubclassOf<AActor> BPClass = BPClassFinder.Class;
...//利用Class生成蓝图对象

注意:

  • FClassFinder的模版名不能直接写UBlueprint,例如:FClassFinder< UBlueprint >是错误的。
    创建蓝图时选择的是什么父类,则写对应的父类名,假如是Actor,那么要写成:FClassFinder< AActor>,否则无法加载成功。

  • FClassFinder的模版名必须和TSubclassOf变量的模版名一致,当然也可使用UClass代替TSubclassOf。实际上TSubclassOf< T >也是UClass,只是更加强调这个Class是从T派生出来的。

  • 在启动游戏时若报错提示找不到文件而崩溃(例如:Default property warnings and errors:Error: COD Constructor (MyGameMode): Failed to find /Game/MyProject/MyBP.MyBP)
    这是因为UE4资源路径的一个规范问题,解决办法有两种:
    1.在copy reference出来的文件路径后面加_C,例如:“Blueprint’/Game/Blueprints/MyBP.MyBP_C’”(_C可以理解为获取Class的意思)。
    2.去掉路径前缀,例如:"/Game/Blueprints/MyBP"

  1. 间接属性引用

间接属性引用用到了前面在介绍软引用时的TSoftObjectPtr。

这种方式是可以控制什么时候加载该资源,并且加载需要手动写代码加载,加载之前将对该资源进行检查看是否加载。如果没有加载,需要使用FStreamingManager 执行同步加载,如果已经加载,则可以直接返回资源对象的指针

示例

//在蓝图中设置完资源后还需要手动判断是否需要加载
TSoftObjectPtr< UStaticMesh > MeshData;

void ASoftActor::DynamicUpdateStaticMesh()
{
	FStreamableManager& StreamableManager = UAssetManager::Get().GetStreamableManager();
	//TSoftObjectPtr.IsPending() :方法可检查资源是否已经加载到内存中,未加载到内存,然后true;已经加载,返回false
	if (MeshData.IsPending())
	{
	    //TSoftObjectPtr.ToSoftObjectPath()来得到FSoftObjectPaths用于加载
		const FSoftObjectPath& AssetRef = MeshData.ToSoftObjectPath();
		//利用FStreamableManager同步加载资源
		MeshData = Cast<UStaticMesh>(StreamableManager.LoadSynchronous(AssetRef));
		//TSoftObjectPtr..Get()如果其引用的资源是否已经载入在内存中,若已载入则返还资源对象指针,否则返还空。
		if (MeshData.Get())
		{
			BaseMesh->SetStaticMesh(MeshData.Get());
		}
	}
	else
	{
		if (MeshData.Get())
		{
			BaseMesh->SetStaticMesh(MeshData.Get());
		}
	}
}

另外
TryLoad/LoadSynchronous

LoadSynchronous():TSoftObjectPtr<T>的方法,也是直接根据路径加载资源。
TryLoad():FSoftObjectPaths的方法,直接根据路径加载资源。
  1. 运行时同步加载

[UE4]C++实现动态加载的问题:LoadClass()和LoadObject()

LoadObject<T>():加载UObject,一般用来加载非蓝图资源,比如动画、贴图、音效等资源;

LoadClass<T>():加载UClass,一般用来加载蓝图资源并获取蓝图Class。比如角色蓝图。
                如果要用蓝图创建对象,必须先通过LoadClass获取class,然后再通过SpawnActor生成对象。

两者均在在UObjectGlobals.h中。

另外有两个函数叫:StaticLoadObject()StaticLoadClass(),是LoadObject()LoadClass()的早期版本,
前两者需要手动强转和填写冗杂参数,后两者则是前两者的封装,使用更方便,推荐使用后者。

这种方式与第二点中提到的构造时引用正好相反,可以实现在运行中加载指定路径的资源。

以下是示例代码:

//LoadObject使用示例
UMaterial *mt = LoadObject<UMaterial>(nullptr, TEXT("/Game/Map/Materials/grass.grass"));

//StaticLoadObject使用示例
UMaterial *mt = Cast<UMaterial>(StaticLoadObject(UMaterial::StaticClass(), nullptr, TEXT("/Game/Map/Materials/grass.grass")));

UStaticMesh* AMyActor::GetStaticMeshByName(const FString& AssetName){
	//"StaticMesh'/Engine/BasicShapes/Cube.Cube'"
	FString Paths = "StaticMesh'/Game/TestFolder/Product1/Geometries/";
	Paths.Append(AssetName).Append(".").Append(AssetName).Append("'");
	UStaticMesh* ReturnMesh = Cast<UStaticMesh>(StaticLoadObject(UStaticMesh::StaticClass(), NULL, *Paths));
	return ReturnMesh;
}

此外,

还有一个可能常用的全局函数FindObject(),用来查询资源是否载入进内存,若存在则返还资源对象指针,否则返还空。但是我们不用先查询再使用LoadXXX,因为LoadXXX里本身就有用到FindObject来检查存在性。

这种方式未提供蓝图接口,要想在蓝图中调用需要用C++进行封装。
C++封装示例:
创建一个FunctionLibrary,写入以下代码:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Engine/Texture2D.h"
#include "LoadFile.generated.h"

/**
 * 
 */
UCLASS()
class TWODTEST_API ULoadFile : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

    UFUNCTION(BlueprintCallable, meta = (DisplayName = "LoadTextureFromPath", keywords = "Load"), Category = "LoadFile")    
    UTexture2D* LoadTextureFromPath(const FString& Path)
    {
        if (Path.IsEmpty()) return NULL;

        return Cast<UTexture2D>(StaticLoadObject(UTexture2D::StaticClass(), NULL, *(Path)));
    }
};

点一下VS的生成和UE4 的compile就能在蓝图中调用了:
在这里插入图片描述

  1. 异步加载

为了避免加载过多资源时造成卡顿,需要进行异步加载

由于软引用里包含资源完整路径名,因此无需再写一次路径名,而是调用如上成员方法来加载资源进内存。

FStreamableManager.RequestAsyncLoad()
首先,需要创建FStreamableManager,官方建议将它放在某类全局游戏单例对象中,例如使用GameSingletonClassName在DefaultEngine.ini中指定的对象。

FStreamableManager.RequestAsyncLoad():将异步加载一组资源并在完成后调用委托。

void UGameCheatManager::GrantItems()
{      
       //获取 FStreamableManager的单例对象引用
       FStreamableManager& Streamable = ...;

       //得到一组软引用
       TArray<FSoftObjectPath> ItemsToStream;
       for(int32 i = 0; i < ItemList.Num(); ++i)
           ItemsToStream.AddUnique(ItemList[i].ToStringReference());

       //根据一组软引用来异步加载一组资源,加载完后调用委托
       Streamable.RequestAsyncLoad(ItemsToStream, FStreamableDelegate::CreateUObject(this, &UGameCheatManager::GrantItemsDeferred));
}

void UGameCheatManager::GrantItemsDeferred()
{
       //do something....
}

FStreamableManager其实也有同步加载的方法:SynchronousLoad()方法将进行一次简单的块加载并返回对象。

  1. 卸载资源

【UE4 C++】UObject 创建、销毁、内存管理
[UE4]UObject ConditionalBeginDestroy相关

如果资源永不再使用,想将资源对象从内存上卸载,代码如下:

Texture2D* mytex; //这里假设mytex合法有效  

mytex->ConditionalBeginDestroy();  
mytex = NULL;  
GetWorld()->ForceGarbageCollection(true); 

创建对象

UE4的对象(即从UObject派生出来的类对象)最好不要用C++的new/delete,而应使用UE4提供的对象生成方法,要不然继承UObject的垃圾回收能力就无从用处。

  1. 创建一般对象

如果有UObject的派生类(非Actor、非Component),那么可使用NewObject()模板函数来创建其实例对象:

UMyObject* MyObject = NewObject<UMyObject>();
  1. 创建Actor派生类对象

生成AActor派生类对象不要用NewObject或new,而要用UWorld::SpawnActor()

UWorld* World = GetWorld();
FVector pos(150, 0, 20);
AMyActor* MyActor = World->SpawnActor<AMyActor>(pos,FRotator::ZeroRotator);

注意SpawnActor不能放在构造函数,但是可以放在其他时期的函数里,例如BeginPlay()Tick()...否则可能会编译后就crash。
  1. 创建Component派生类对象

为Actor创建组件,可使用UObject::CreateDefaultSubobject()模板函数

UCameraComponent* Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera0"));

CreateDefaultSubobject必须写在Actor的无参构造函数中,否则crash。
CreateDefaultSubobject中的TEXT或者FName参数在同一个Actor中不能重复,否则crash。
一定要添加RegisterComponent(),否则编辑器不会显示。
  1. 创建蓝图对象

蓝图由于本质是一种脚本,不是直接的C++类,因此往往需要借助动态类型来生成蓝图对象。所有的加载资源并创建到场景中的方式都离不开SpawnActor这一句代码。

  • 通过已确定的父类来生成蓝图对象
AMyActor* spawnActor = GetWorld()->SpawnActor<AMyActor>(AMyActor::StaticClass());  
如果你的蓝图派生于某个C++,那么可以直接访问该类的StaticClass()并用于SpawnActor来创建蓝图对象。
  • 通过UClass生成蓝图对象
UClass* BPClass = LoadClass<AActor>(nullptr, TEXT("/Game/Blueprints/MyBP"));    //TSubclassOf<AActor>同理
AActor* spawnActor = GetWorld()->SpawnActor<AActor>(BPClass);
  • 通过UObject生成蓝图对象
若得到UObject则需要先转换成UBlueprint,再通过GeneratedClass获取UClass来生成蓝图对象

FStringAssetReference asset = "Blueprint'/Game/BluePrint/TestObj.TestObj'";  
UObject* itemObj = asset.ResolveObject();  
UBlueprint* gen = Cast<UBlueprint>(itemObj); 
if (gen != NULL){  
    AActor* spawnActor = GetWorld()->SpawnActor<AActor>(gen->GeneratedClass);  
}
  1. 拷贝Actor派生类对象
AActor* CloneActor(AActor* InputActor)
{
   UWorld * World = InputActor->GetWorld();
   FActorSpawnParameters params;
   params.Template = InputActor;

	UClass* ItemClass = InputActor->GetClass();
	AActor* const SpawnedActor = World->SpawnActor<AActor>(ItemClass, params);
	return SpawnedActor;
}

UE4 AssetManager

AssetManager是一个单例的UObject,它是UE4提供给开发者在runtime的时候进行查询以及读取Assets的资源管理类。

一般情况下UE4会自动加载和卸载资源,然而我们有些时候项目需要对资源有更多地掌控则就需要它来帮忙了。

具体:

【UE4】资源管理之UAssetManager用法

Unreal Engine 4 —— Asset Manager介绍

Unreal Engine 4 —— Fortnite中的Asset Manager与资源控制

资源查找

(UE4 4.20)UE4获取所有特定资源(FAssetData)的资源路径

待续。。。

实践

C++

待续。。。

蓝图

  1. 蓝图中根据路径名(字符串)来动态地加载图片纹理到UI的Image控件上

参考:

UE4 Image Asset From String
为UE4制作实时加载界面(蓝图向)

首先:图片是位于哪里?Web,电脑的文件,或者UE项目的uasset资源中?

  • 图片位于UE项目的uasset资源中
    在这里插入图片描述
  • 图片位于电脑的文件中
    在这里插入图片描述
    这里还有一个按名称加载多个图片的示例(其中,Victory Load Texture 2D from File是插件提供的节点,这里只是一个截图示例),没有该插件可以用Import File as Texture 2D代替。
    在这里插入图片描述
  • 图片位于Web上
    在这里插入图片描述
  1. 蓝图中根据视频名字(字符串)来动态地加载媒体源,实现视频播放时动态加载

先遍历文件夹,得到视频名字字符串映射到SoftObjectPath的字典
在这里插入图片描述
然后根据上面视频名字字符串映射到SoftObjectPath的字典去按需加载视频,生成UE中播放视频时用到的媒体文件源
在这里插入图片描述

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-11-11 13:02:12  更:2021-11-11 13:03:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/28 0:31:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码