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 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> Unreal-GAS 源码解析 网络同步 -> 正文阅读

[系统运维]Unreal-GAS 源码解析 网络同步

GameplayAbility

赋予GA

UGameplayAbility* Ability

AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(Ability, 0, INDEX_NONE, this))

// ActivatableAbilities 所有可激活的能力数组
UPROPERTY(ReplicatedUsing=OnRep_ActivateAbilities, BlueprintReadOnly, Category = "Abilities")
FGameplayAbilitySpecContainer ActivatableAbilities;

// This property will only send to the replay connection, or to the actors owner
Params.Condition = COND_ReplayOrOwner;
DOREPLIFETIME_WITH_PARAMS_FAST(UAbilitySystemComponent, ActivatableAbilities, Params);
FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility(const FGameplayAbilitySpec& Spec)
	FGameplayAbilitySpec& OwnedSpec = ActivatableAbilities.Items[ActivatableAbilities.Items.Add(Spec)];
	if (OwnedSpec.Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor)
		CreateNewInstanceOfAbility(OwnedSpec, Spec.Ability);
	// 标记为Dirty,使Spec复制到客户端
	MarkAbilitySpecDirty(OwnedSpec, true);

GA实例化策略

namespace EGameplayAbilityInstancingPolicy
	enum Type
		// 使用CDO执行业务逻辑,无法存在状态信息
		NonInstanced,
		// 每个Actor实例化一个,注意多次触发之间的延时行为
		InstancedPerActor,
		// 每次执行都创建实例
		InstancedPerExecution

创建GASpec

FGameplayAbilitySpec::FGameplayAbilitySpec(UGameplayAbility* InAbility, int32 InLevel, int32 InInputID, UObject* InSourceObject)
	: Ability(InAbility)
	, Level(InLevel)
	, InputID(InInputID)
	, SourceObject(InSourceObject)
	...
		// 创建时获取一个Handle
		Handle.GenerateNewHandle();
void FGameplayAbilitySpecHandle::GenerateNewHandle()
	static int32 GHandle = 1;
	Handle = GHandle++;

创建GA实例

UGameplayAbility* UAbilitySystemComponent::CreateNewInstanceOfAbility(FGameplayAbilitySpec& Spec, UGameplayAbility* Ability)
	// 使用GA CDO创建GA Instance
	if (Ability->HasAllFlags(RF_ClassDefaultObject))
		AbilityInstance = NewObject<UGameplayAbility>(Owner, Ability->GetClass());
	else
		AbilityInstance = NewObject<UGameplayAbility>(Owner, Ability->GetClass(), NAME_None, RF_NoFlags, Ability);
	// 存入GA Instance数组
	if (AbilityInstance->GetReplicationPolicy() != EGameplayAbilityReplicationPolicy::ReplicateNo)
		Spec.ReplicatedInstances.Add(AbilityInstance);
		AllReplicatedInstancedAbilities.Add(AbilityInstance);
	else
		Spec.NonReplicatedInstances.Add(AbilityInstance);

激活GA

通过Handle激活GA

bool UAbilitySystemComponent::TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation)
	FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(AbilityToActivate);
	// GA CDO
	UGameplayAbility* Ability = Spec->Ability;
	// 跳过模拟端
	if (NetMode == ROLE_SimulatedProxy)
		return false;
	// Local Only Filter: ClientTryActivateAbility
	...
	// Server Only Filter: CallServerTryActivateAbility
	...
	return InternalTryActivateAbility(AbilityToActivate);

当然,有其它上层的激活方法:

  • 通过Tag激活GA
bool UAbilitySystemComponent::TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation)
	TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
	// 找到所有可激活的匹配Tag的GA
	GetActivatableGameplayAbilitySpecsByAllMatchingTags(GameplayTagContainer, AbilitiesToActivate);
		for (const FGameplayAbilitySpec& Spec : ActivatableAbilities.Items)
			// 匹配Tag
			if (Spec.Ability && Spec.Ability->AbilityTags.HasAll(GameplayTagContainer))
				// 满足其它限制条件
				if (!bOnlyAbilitiesThatSatisfyTagRequirements || Spec.Ability->DoesAbilitySatisfyTagRequirements(*this))
					MatchingGameplayAbilities.Add(const_cast<FGameplayAbilitySpec*>(&Spec));
	bSuccess |= TryActivateAbility(GameplayAbilitySpec->Handle, bAllowRemoteActivation);

是否有Tag的Miss、Block

bool UGameplayAbility::DoesAbilitySatisfyTagRequirements(const UAbilitySystemComponent& AbilitySystemComponent, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
	bool bBlocked = false;
	bool bMissing = false;
  • 通过GA类激活GA
bool TryActivateAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToActivate, bool bAllowRemoteActivation = true)
	// 判断CDO是否相同
	if (Spec.Ability == InAbilityCDO)
  • 通过Event激活GA
bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);
	if (Ability->ShouldAbilityRespondToEvent(ActorInfo, &TempEventData))

底层的激活函数都是InternalTryActivateAbility:

bool UAbilitySystemComponent::InternalTryActivateAbility(FGameplayAbilitySpecHandle Handle, FPredictionKey InPredictionKey, UGameplayAbility** OutInstancedAbility, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData)
	FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
	UGameplayAbility* const CanActivateAbilitySource = InstancedAbility ? InstancedAbility : Ability;
	// 过程中维护InternalTryActivateAbilityFailureTags,讲失败原因通过Tag进行记录
	if (!CanActivateAbilitySource->CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, &InternalTryActivateAbilityFailureTags))
		NotifyAbilityFailed(Handle, CanActivateAbilitySource, InternalTryActivateAbilityFailureTags);
		return false;
	// InstancedPerActor下禁止重复激活
	if (Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor)
		if (Spec->IsActive())
			...
	Spec->ActiveCount++;
	// 见下
	if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalOnly || (NetRole == ROLE_Authority))
		// PredictionKey
		// ClientActivateAbilitySucceed
		// MulticastActivateAbilitySucceed
		// CallActivateAbility
	else if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)
		// PredictionKey
		// CallServerTryActivateAbility
		// CallActivateAbility

是否可以激活

bool UGameplayAbility::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
	!ShouldActivateAbility(AvatarActor->GetLocalRole())
	// 检测是否有Cooldown Tag
	!CheckCooldown(Handle, ActorInfo, OptionalRelevantTags)
	// 通过CanApplyAttributeModifiers判断
	!CheckCost(Handle, ActorInfo, OptionalRelevantTags)
	!DoesAbilitySatisfyTagRequirements(*AbilitySystemComponent, SourceTags, TargetTags, OptionalRelevantTags)
	// Input阻塞
	AbilitySystemComponent->IsAbilityInputBlocked(Spec->InputID)
	// 蓝图自定义
	!K2_CanActivateAbility(*ActorInfo, Handle, OutTags)

最后的执行部分

void UGameplayAbility::CallActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData)
	PreActivate(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate);
	ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
// 自己去重载 Do something
void UGameplayAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
	// 可拆为CommitCost()、CommitCooldown()
	// CommitCheck、CommitExecute
	if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
		EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
	...

可以自己设置Timer或者回调调用EndAbility或者CancelAbility

EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
void UGameplayAbility::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)

在示例中,ActivateAbility创建GameplayTask进行非瞬时操作,通过绑定OnCompleted、OnCancelled、EventReceived进行GA流程控制

激活GA - 预测

执行策略:

namespace EGameplayAbilityNetExecutionPolicy
	enum Type
		// Client上传并本地预测
		LocalPredicted		UMETA(DisplayName = "Local Predicted"),
		// Client不上传
		LocalOnly			UMETA(DisplayName = "Local Only"),
		// Client上传,等Server
		ServerInitiated		UMETA(DisplayName = "Server Initiated"),
		// Server不下发
		ServerOnly			UMETA(DisplayName = "Server Only"),

通过PKey判断需要取消哪些GA

UAbilitySystemComponent::InternalTryActivateAbility
	// ROLE_Authority && (!InPredictionKey || ServerInitiated || ServerOnly)
	// Server新建PKey
	if (bCreateNewServerKey)	
		ActivationInfo.ServerSetActivationPredictionKey(FPredictionKey::CreateNewServerInitiatedKey(this));
	// 使用传入PKey
	else if (InPredictionKey.IsValidKey())
		ActivationInfo.ServerSetActivationPredictionKey(InPredictionKey);
	// NetExecutionPolicy: LocalPredicted
	// 见下
	FScopedPredictionWindow ScopedPredictionWindow(this, ActivationInfo.GetActivationPredictionKey());
	CallServerTryActivateAbility(Handle, Spec->InputPressed, ScopedPredictionKey);
		// RPC Server
		ServerTryActivateAbility(AbilityHandle, InputPressed, PredictionKey);
			// Server
			InternalServerTryActivateAbility(Handle, InputPressed, PredictionKey, nullptr);
				// 找到Server上的该技能(Handle的作用)
				FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
				FScopedPredictionWindow ScopedPredictionWindow(this, PredictionKey);
				// Server激活GA
				InternalTryActivateAbility(Handle, PredictionKey, &InstancedAbility, nullptr, TriggerEventData)
				// 如果过程中失败,会回调通知Client
				ClientActivateAbilityFailed(Handle, PredictionKey.Current);

Server失败的RPC通知

void UAbilitySystemComponent::ClientActivateAbilityFailed_Implementation(FGameplayAbilitySpecHandle Handle, int16 PredictionKey)
	FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
	if (Spec->ActivationInfo.GetActivationPredictionKey().Current == PredictionKey)
		Spec->ActivationInfo.SetActivationRejected();
			ActivationMode = EGameplayAbilityActivationMode::Rejected;
	// 取消所有相同PKey的GA
	TArray<UGameplayAbility*> Instances = Spec->GetAbilityInstances();
	for (UGameplayAbility* Ability : Instances)
		if (Ability->CurrentActivationInfo.GetActivationPredictionKey().Current == PredictionKey)
			Ability->K2_EndAbility();
				EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);

结束技能

void UGameplayAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
	// 移除timers、latent actions
	MyWorld->GetLatentActionManager().RemoveActionsForObject(this);
	if (FAbilitySystemTweaks::ClearAbilityTimers)
		MyWorld->GetTimerManager().ClearAllTimersForObject(this);
	Task->TaskOwnerEnded();
	// 通知远端
	AbilitySystemComponent->ReplicateEndOrCancelAbility(Handle, ActivationInfo, this, false);
	// 移除Tag
	AbilitySystemComponent->RemoveLooseGameplayTags(ActivationOwnedTags);
	// 移除Cue
	AbilitySystemComponent->RemoveGameplayCue(GameplayCueTag);
	AbilitySystemComponent->HandleChangeAbilityCanBeCanceled(AbilityTags, this, false);
	AbilitySystemComponent->ApplyAbilityBlockAndCancelTags(AbilityTags, this, false, BlockAbilitiesWithTag, false, CancelAbilitiesWithTag);

GameplayEffect

施加GE

FActiveGameplayEffectHandle UGameplayAbility::ApplyGameplayEffectSpecToOwner(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle) const
	if (SpecHandle.IsValid() && (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo)))
		return AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get(), AbilitySystemComponent->GetPredictionKeyForNewAction());

bool UGameplayAbility::HasAuthorityOrPredictionKey(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo* ActivationInfo) const
	bool UAbilitySystemComponent::HasAuthorityOrPredictionKey(const FGameplayAbilityActivationInfo* ActivationInfo) const
		bool UAbilitySystemComponent::CanPredict() const
			bool IsValidForMorePrediction() const
				return Current > 0 && bIsStale == false && bIsServerInitiated == false;

FPredictionKey GetPredictionKeyForNewAction() const
	return ScopedPredictionKey.IsValidForMorePrediction() ? ScopedPredictionKey : FPredictionKey();
FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(const FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
	// 周期GE
	if(PredictionKey.IsValidKey() && Spec.GetPeriod() > 0.f)
		if(IsOwnerActorAuthoritative())
			PredictionKey = FPredictionKey();
		else
			return FActiveGameplayEffectHandle();
	// HasApplicationImmunityToSpec
	// OwnedGameplayTags -> ApplicationTagRequirements RemovalTagRequirements
	// CustomApplicationRequirement
	// 瞬时GE的执行
	FGameplayEffectSpec* OurCopyOfSpec = nullptr;
	StackSpec = TSharedPtr<FGameplayEffectSpec>(new FGameplayEffectSpec(Spec));
	OurCopyOfSpec = StackSpec.Get();
	UAbilitySystemGlobals::Get().GlobalPreGameplayEffectSpecApply(*OurCopyOfSpec, this);
	OurCopyOfSpec->CaptureAttributeDataFromTarget(this);
	UAbilitySystemGlobals::Get().SetCurrentAppliedGE(OurCopyOfSpec);
	ExecuteGameplayEffect(*OurCopyOfSpec, PredictionKey);

预测GE

当本地预测Instant GE的时候

FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(const FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
	bool bTreatAsInfiniteDuration = GetOwnerRole() != ROLE_Authority && PredictionKey.IsLocalClientKey() && Spec.Def->DurationPolicy == EGameplayEffectDurationType::Instant;
	if (Spec.Def->DurationPolicy != EGameplayEffectDurationType::Instant || bTreatAsInfiniteDuration)
		AppliedEffect = ActiveGameplayEffects.ApplyGameplayEffectSpec(Spec, PredictionKey, bFoundExistingStackableGE);

将预测失败删除GE的函数绑定到PKey的同步,PKey同步说明了已经收到了服务器的GE,那么本地预测的部分就可以删除了

FActiveGameplayEffect* FActiveGameplayEffectsContainer::ApplyGameplayEffectSpec(const FGameplayEffectSpec& Spec, FPredictionKey& InPredictionKey, bool& bFoundExistingStackableGE)
	AppliedActiveGE = new FActiveGameplayEffect(NewHandle, Spec, GetWorldTime(), GetServerWorldTime(), InPredictionKey);
	*PendingGameplayEffectNext = AppliedActiveGE;
	if (!(InPredictionKey.IsLocalClientKey() == false || IsNetAuthority()))
		InPredictionKey.NewRejectOrCaughtUpDelegate(FPredictionKeyEvent::CreateUObject(Owner, &UAbilitySystemComponent::RemoveActiveGameplayEffect_NoReturn, AppliedActiveGE->Handle, -1));
	void FActiveGameplayEffectsContainer::InternalOnActiveGameplayEffectAdded(FActiveGameplayEffect& Effect)

PKey使用FReplicatedPredictionKeyMap作为容器:

UPROPERTY(Replicated)
FReplicatedPredictionKeyMap UAbilitySystemComponent::ReplicatedPredictionKeyMap;

Params.Condition = COND_OwnerOnly;
DOREPLIFETIME_WITH_PARAMS_FAST(UAbilitySystemComponent, ReplicatedPredictionKeyMap, Params);

再看ScopedPredictionWindow的作用:在Scope结束后,恢复之前的PKey,并把当前的PKey加入ReplicatedPredictionKeyMap

FScopedPredictionWindow::FScopedPredictionWindow(UAbilitySystemComponent* AbilitySystemComponent, FPredictionKey InPredictionKey, bool InSetReplicatedPredictionKey /*=true*/)
	if (AbilitySystemComponent->IsNetSimulating() == false)
		Owner = AbilitySystemComponent;
		// 设置ScopedPredictionKey,存下原先的ScopedPredictionKey
		RestoreKey = AbilitySystemComponent->ScopedPredictionKey;
		AbilitySystemComponent->ScopedPredictionKey = InPredictionKey;
		ClearScopedPredictionKey = true;
		SetReplicatedPredictionKey = InSetReplicatedPredictionKey;

FScopedPredictionWindow::~FScopedPredictionWindow()
	if (SetReplicatedPredictionKey)
		// 记录已经结束的Scope的PKey
		if (OwnerPtr->ScopedPredictionKey.IsValidKey())
			// 加入环形数组并同步,这里是预测的关键
			OwnerPtr->ReplicatedPredictionKeyMap.ReplicatePredictionKey(OwnerPtr->ScopedPredictionKey);
				PredictionKeys[Index].PredictionKey = Key;
				MarkItemDirty(PredictionKeys[Index]);
			OwnerPtr->bIsNetDirty = true;
	// 恢复ScopedPredictionKey
	if (ClearScopedPredictionKey)
		OwnerPtr->ScopedPredictionKey = RestoreKey;

ReplicatedPredictionKeyMap同步时:

bool FReplicatedPredictionKeyMap::NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
	return FastArrayDeltaSerialize<FReplicatedPredictionKeyItem>(PredictionKeys, DeltaParms, *this);

之后会调到

FReplicatedPredictionKeyItem
	void PostReplicatedAdd(const struct FReplicatedPredictionKeyMap &InArray) { OnRep(); }
	void PostReplicatedChange(const struct FReplicatedPredictionKeyMap &InArray) { OnRep(); }

在PKey同步的时候会触发委托

void FReplicatedPredictionKeyItem::OnRep()
	FPredictionKeyDelegates::CatchUpTo(PredictionKey.Current);
		for (auto& Delegate : DelPtr->CaughtUpDelegates)
			Delegate.ExecuteIfBound();
				void RemoveActiveGameplayEffect_NoReturn(FActiveGameplayEffectHandle Handle, int32 StacksToRemove=-1)
					UAbilitySystemComponent::RemoveActiveGameplayEffect(Handle, StacksToRemove);
						bool FActiveGameplayEffectsContainer::RemoveActiveGameplayEffect(FActiveGameplayEffectHandle Handle, int32 StacksToRemove)
							InternalRemoveActiveGameplayEffect(ActiveGEIdx, StacksToRemove, true);

移除GE

bool FActiveGameplayEffectsContainer::InternalRemoveActiveGameplayEffect(int32 Idx, int32 StacksToRemove, bool bPrematureRemoval)
	FActiveGameplayEffect& Effect = *GetActiveGameplayEffect(Idx);
	// 清除层数
	if (StacksToRemove > 0 && Effect.Spec.StackCount > StacksToRemove)
		Effect.Spec.StackCount -= StacksToRemove;
		return false;
	// 移除Tag和Magnitude
	InternalOnActiveGameplayEffectRemoved(Effect, ShouldInvokeGameplayCueEvent, GameplayEffectRemovalInfo);
	// 持续
	Owner->GetWorld()->GetTimerManager().ClearTimer(Effect.DurationHandle);
	// 周期
	Owner->GetWorld()->GetTimerManager().ClearTimer(Effect.PeriodHandle);
	// 移除后施加其它GE
	InternalApplyExpirationEffects(Effect.Spec, bPrematureRemoval);
  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-02-26 12:11:44  更:2022-02-26 12:12:20 
 
开发: 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/16 5:41:11-

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