使用Entitas构建游戏框架之Entitas源码解读

  • 前言
  • 一、为什么要ECS
  • 二、Entitas介绍
    • 源码介绍与使用:
      • 1. Component
      • 2. Entity
      • 3. Matcher
      • 4. Group
      • 5. Collector
      • 6. Context
      • 7. System
  • 三、为什么不用unity自带的ECS
  • 总结

前言

解耦在编程时是很重要的一个考量因素,但是如果能在框架上就增加约束,让代码看上去更清晰,那么即便有一些新手,在这种约束下,也不会耦合太严重。暴雪在GDC 2017年分享了《守望先锋》关于ECS系统的实施方案,给出了一种框架上解耦的答案。虽然这种面向数据编程的思想其实很早就有了,但是《守望先锋》成功让ECS模式更广为人知。
Entitas其实早在《守望先锋》之前就已经有了,是一个超快的实体组件系统框架(ECS),专门为c#和Unity制作。内部缓存和惊人的快速组件访问使它成为首屈一指的框架。


一、为什么要ECS

在ECS之前,大部分游戏框架都是基于面向对象的,面向对象有个特点就是封装,即把一个对象的相关的属性以及对象的行为方法放到一起。大多数游戏都有主角,在使用面向对象编程时,一般游戏主角的class都有,属性上百个,方法上百个,更是能达到上万行的代码,这就导致一个新来的程序员看到后,很难阅读,访问class的方法也散布到游戏的各个地方,耦合在了各个模块里。如下图所示,存在大量的otherClass调用MainCharacter的方法以及访问属性,代码散布到各个otherClass里面,在清理时也面临复杂的引用链难以处理的问题。

而ECS不同,ECS将面向对象中的属性跟方法分离,属性放在各个component里面,这些component包含在一个Entity里面,而方法被封装成一个一个的system,这些system处理拥有相同一些component的一组Enetity。这样找属性就去找component,找逻辑就去看相关的system,这就不需要去遍历一个上万行的文件,是多么轻松的一件事。如下图所示,整个游戏是一个world,包含一组system以及所有的entity,每个entity是部分component的集合。这样清晰的结构,即便写出一些耦合的代码,也只在某个system或者某个组件里,写功能的人不需要去阅读所有的逻辑代码以及去遍历component,在清理对象的时候,由于关系链清晰,因此也变得很容易。
​​​​​​

二、Entitas介绍

Entitas是一个开源的,轻量级的,超快的实体组件系统框架(ECS),专门为c#和Unity制作。内部缓存和惊人的快速组件访问使它成为首屈一指的框架。

源码地址:https://github.com/sschmid/Entitas-CSharp


源码介绍与使用:

先给出github上entitas的图说明:

1. Component

Entitas中,所有的逻辑属性都要求保存在Component中,在Entitas中所有声明的component都要继承IComponent接口,而Component除了一些功能型的util方法以外,是不能有逻辑代码的。

IComponent源码如下:

namespace Entitas {public interface IComponent {}
}

这里我们需要添加一个interface:IResetable,原因是component都是池化对象,防止dirty:

namespace Entitas
{public interface IResetable{void Reset();}
}

因此添加一个抽象类:

using Entitas;namespace SHH.Share.Component
{public abstract class BaseComponent : IComponent, IResetable{public virtual void Reset(){}}
}

我们声明一个组件可以像下面一样:

using SHH.Math;
using Entitas;
using SHH.Share.Component;namespace SHH
{public partial class PlayerPositionComponent : BaseComponent{public float3 Position;public override void Reset(){base.Reset();Position = float3.zero;}}public partial class PlayerAimRotComponent : BaseComponent{public float3 AimRotation;public override void Reset(){base.Reset();AimRotation = float3.zero;}}
}

2. Entity

实体是存储数据的容器,用于表示应用程序中的某些对象。您可以以iccomponent的形式从实体中添加、替换或删除component。实体有相应的事件让您知道组件是否被添加、替换或删除。Entity继承了IEntity,其实这里IEntity也只有Entity继承。

IEntity部分源码如下:

    public delegate void EntityComponentChanged(IEntity entity, int index, IComponent component, ushort opFlags);public delegate void EntityComponentReplaced(IEntity entity, int index, IComponent previousComponent, IComponent newComponent, ushort opFlags);public delegate void EntityEvent(IEntity entity);public interface IEntity : IAERC {event EntityComponentChanged OnComponentAdded;event EntityComponentChanged OnComponentRemoved;event EntityComponentReplaced OnComponentReplaced;event EntityEvent OnEntityReleased;event EntityEvent OnDestroyEntity;int totalComponents { get; }int creationIndex { get; }bool isEnabled { get; }Stack<IComponent>[] componentPools { get; }ContextInfo contextInfo { get; }IAERC aerc { get; }void Initialize(int creationIndex,int totalComponents,Stack<IComponent>[] componentPools,ContextInfo contextInfo = null,IAERC aerc = null);void Reactivate(int creationIndex);void AddComponent(int index, IComponent component, ushort opFlags);void RemoveComponent(int index,ushort opFlags);void ReplaceComponent(int index, IComponent component, ushort opFlags);IComponent GetComponent(int index);IComponent[] GetComponents();int[] GetComponentIndices();bool HasComponent(int index);bool HasComponents(int[] indices);bool HasAnyComponent(int[] indices);void RemoveAllComponents();Stack<IComponent> GetComponentPool(int index);IComponent CreateComponent(int index, Type type);T CreateComponent<T>(int index) where T : new();void Destroy();void InternalDestroy();void RemoveAllOnEntityReleasedHandlers();}

IEntity主要描述了Entity的基本描述,比方说组件数量,component的行为,以及池化相关等

而Entity继承自IEntity,实现了上述方法,增加了字段

IComponent[] _components;public void Initialize(int creationIndex, int totalComponents, Stack<IComponent>[] componentPools, ContextInfo contextInfo = null, IAERC aerc = null)
{Reactivate(creationIndex);_totalComponents = totalComponents;_components = new IComponent[totalComponents];_componentPools = componentPools;_contextInfo = contextInfo ?? createDefaultContextInfo();_aerc = aerc ?? new SafeAERC(this);}

这就是我们关键的数据组,其他的操作都是辅助_components行为的,只初始化Entity的时候,就会创建一个IComponent数组,大小由totalComponents决定。也就是说,每个entity的组件数量最开始就决定了,而每一个数组index就表示了特定的组件,这就给我们筛选相同组件的entity带来了可能。

3. Matcher

ECS中的每个System其实是对拥有相同一组component的Entity的处理,那么要匹配Entity的哪些组件呢,由Matcher指定。

Matcher有如下字段

public int[] indices {get {if (_indices == null) {_indices = mergeIndices(_allOfIndices, _anyOfIndices, _noneOfIndices);}return _indices;}
}public int[] allOfIndices { get { return _allOfIndices; } }
public int[] anyOfIndices { get { return _anyOfIndices; } }
public int[] noneOfIndices { get { return _noneOfIndices; } }public string[] componentNames { get; set; }int[] _indices;
int[] _allOfIndices;
int[] _anyOfIndices;
int[] _noneOfIndices;

这些字段保存了一个Entity需要的Component index数组,其中

  • allOfIndices :表示需要每个component index代表的组件都要有
  • anyOfIndices :表示有其中的一个就行
  • noneOfIndices :表示这里面的index都没有

通过这些index进行Entity 筛选:

public bool Matches(Entity entity) {return (_allOfIndices == null || entity.HasComponents(_allOfIndices))&& (_anyOfIndices == null || entity.HasAnyComponent(_anyOfIndices))&& (_noneOfIndices == null || !entity.HasAnyComponent(_noneOfIndices));}

4. Group

Group支持对上下文中的实体进行超快速过滤。当实体发生变化时,它们会不断更新,并可以立即返回实体组。假设你有成千上万个实体,你只想要那些有一个PlayerPositionComponent 的实体-只要询问这个组的上下文,它已经有结果在等待你。
Group继承自IGroup,还是先看interface:

namespace Entitas {public delegate void GroupChanged(IGroup group, Entity entity, int index, IComponent component);public delegate void GroupUpdated(IGroup group, Entity entity, int index, IComponent previousComponent, IComponent newComponent);public interface IGroup : IEnumerable<Entity> {int count { get; }void RemoveAllEventHandlers();event GroupChanged OnEntityAdded;event GroupChanged OnEntityRemoved;event GroupUpdated OnEntityUpdated;IMatcher matcher { get; }void HandleEntitySilently(Entity entity);void HandleEntity(Entity entity, int index, IComponent component);GroupChanged HandleEntity(Entity entity);void UpdateEntity(Entity entity, int index, IComponent previousComponent, IComponent newComponent);bool ContainsEntity(Entity entity);Entity[] GetEntities();Entity GetSingleEntity();}
}

可以看到IGroup中有个matcher,一个entity的count,以及一组entity变化后的行为,在这里就应该知道,Group的主要功能是通过matcher缓存了一组Entity
下面看下Group的部分源码:

namespace Entitas {public class Group : IGroup {/// Returns the number of entities in the group.public int count { get { return _entities.Count; } }/// Returns the matcher which was used to create this group.public IMatcher matcher { get { return _matcher; } }readonly IMatcher _matcher;readonly HashSet<Entity> _entities = new HashSet<Entity>(EntityEqualityComparer.comparer);Entity[] _entitiesCache;Entity _singleEntityCache;string _toStringCache;/// Use context.GetGroup(matcher) to get a group of entities which match/// the specified matcher.public Group(IMatcher matcher) {_matcher = matcher;}}
}

可以看到,Group构造函数传进来了一个matcher,然后使用_entities保存了能通过matcher的Entity,而在OnEntityAdded,OnEntityRemoved的时候会对_entities进行更新

5. Collector

一个Collector可以从相同的Context中观察一个或多个Group,并且根据指定的groupEvent收集更改的实体。
其中groupEvent有如下事件:

public enum GroupEvent : byte {Added,Removed,AddedOrRemoved,Modified,
}

Collector部分源码:

public HashSet<Entity> collectedEntities { get { return _collectedEntities; } }
public int count { get { return _collectedEntities.Count; } }
readonly HashSet<Entity> _collectedEntities;
readonly IGroup[] _groups;
readonly GroupEvent[] _groupEvents;public Collector(IGroup[] groups, GroupEvent[] groupEvents) {_groups = groups;_collectedEntities = new HashSet<Entity>(EntityEqualityComparer.comparer);_groupEvents = groupEvents;if (groups.Length != groupEvents.Length) {throw new CollectorException("Unbalanced count with groups (" + groups.Length +") and group events (" + groupEvents.Length + ").","Group and group events count must be equal.");}_addEntityCache = addEntity;_updateEntityCache = updateEntity;Activate();}

在Collector构造函数中指定了IGroup[] groups, GroupEvent[] groupEvents,每一个group对应注册一个groupEvent。随后调用Activate方法:

public void Activate() {for (int i = 0; i < _groups.Length; i++) {var group = _groups[i];var groupEvent = _groupEvents[i];switch (groupEvent) {case GroupEvent.Added:group.OnEntityAdded -= _addEntityCache;group.OnEntityAdded += _addEntityCache;break;case GroupEvent.Removed:group.OnEntityRemoved -= _addEntityCache;group.OnEntityRemoved += _addEntityCache;break;case GroupEvent.AddedOrRemoved:group.OnEntityAdded -= _addEntityCache;group.OnEntityAdded += _addEntityCache;group.OnEntityRemoved -= _addEntityCache;group.OnEntityRemoved += _addEntityCache;break;case GroupEvent.Modified:group.OnEntityUpdated -= _updateEntityCache;group.OnEntityUpdated += _updateEntityCache;break;}}}

这样在每个groupEntity发生改变的时候,就在addEntity或者updateEntity中收集Entity到_collectedEntities中,然后就可以通过如下代码,对这些改变进行处理:

foreach (var e in collector.collectedEntities) {// do something with all the entities// that have been collected to this point of time
}
collector.ClearCollectedEntities();

6. Context

Context是创建和销毁实体的工厂。他保存了所有的entity对象以及group,可以使用它来过滤感兴趣的实体。
先看下Context继承的接口IContext:

public interface IContext {event ContextEntityChanged OnEntityCreated;event ContextEntityChanged OnEntityWillBeDestroyed;event ContextEntityChanged OnEntityDestroyed;event ContextGroupChanged OnGroupCreated;int totalComponents { get; }Stack<IComponent>[] componentPools { get; }ContextInfo contextInfo { get; }int count { get; }int reusableEntitiesCount { get; }int retainedEntitiesCount { get; }void DestroyAllEntities();void ResetCreationIndex();void ClearComponentPool(int index);void ClearComponentPools();void Reset();Entity CreateEntity(bool isTemp = false);// TODO Obsolete since 0.42.0, April 2017[Obsolete("Please use entity.Destroy()")]void DestroyEntity(Entity entity);bool HasEntity(Entity entity);Entity[] GetEntities();IGroup GetGroup(IMatcher matcher);
}

根据接口的内容基本可以确定这个类的作用,对entity的管理,component池化管理,group缓存。
然后看一下Context类的属性跟构造函数:

public class Context : IContext
{/// The total amount of components an entity can possibly have./// This value is generated by the code generator,/// e.g ComponentLookup.TotalComponents.public int totalComponents { get { return _totalComponents; } }/// Returns all componentPools. componentPools is used to reuse/// removed components./// Removed components will be pushed to the componentPool./// Use entity.CreateComponent(index, type) to get a new or reusable/// component from the componentPool.public Stack<IComponent>[] componentPools { get { return _componentPools; } }/// The contextInfo contains information about the context./// It's used to provide better error messages.public ContextInfo contextInfo { get { return _contextInfo; } }/// Returns the number of entities in the context.public int count { get { return _entities.Count; } }/// Returns the number of entities in the internal ObjectPool/// for entities which can be reused.public int reusableEntitiesCount { get { return _reusableEntities.Count; } }/// Returns the number of entities that are currently retained by/// other objects (e.g. Group, Collector, ReactiveSystem).public int retainedEntitiesCount { get { return _retainedEntities.Count; } }readonly int _totalComponents;readonly Stack<IComponent>[] _componentPools;readonly ContextInfo _contextInfo;readonly Func<IEntity, IAERC> _aercFactory;readonly HashSet<Entity> _entities = new HashSet<Entity>(EntityEqualityComparer.comparer);readonly Stack<Entity> _reusableEntities = new Stack<Entity>();readonly HashSet<Entity> _retainedEntities = new HashSet<Entity>(EntityEqualityComparer.comparer);readonly Dictionary<string, IEntityIndex> _entityIndices;readonly Dictionary<IMatcher, IGroup> _groups = new Dictionary<IMatcher, IGroup>();readonly List<IGroup>[] _groupsForIndex;readonly IGroup[] _groupForSingle;readonly ObjectPool<List<GroupChanged>> _groupChangedListPool;readonly Dictionary<int, Entity> _entitiesLookup = new Dictionary<int, Entity>();int _creationIndex;Entity[] _entitiesCache;/// The prefered way to create a context is to use the generated methods/// from the code generator, e.g. var context = new GameContext();public Context(int totalComponents, int startCreationIndex, ContextInfo contextInfo, Func<IEntity, IAERC> aercFactory){_totalComponents = totalComponents;_creationIndex = startCreationIndex;if (contextInfo != null){_contextInfo = contextInfo;if (contextInfo.componentTypeInfo.componentNames.Length != totalComponents){throw new ContextInfoException(this, contextInfo);}}else{_contextInfo = createDefaultContextInfo();}_aercFactory = aercFactory == null? (entity) => new SafeAERC(entity): aercFactory;_groupsForIndex = new List<IGroup>[totalComponents];_groupForSingle = new IGroup[totalComponents];_componentPools = new Stack<IComponent>[totalComponents];_entityIndices = new Dictionary<string, IEntityIndex>();_groupChangedListPool = new ObjectPool<List<GroupChanged>>(() => new List<GroupChanged>(),list => list.Clear());// Cache delegates to avoid gc allocations_cachedEntityChanged = updateGroupsComponentAddedOrRemoved;_cachedComponentReplaced = updateGroupsComponentReplaced;_cachedEntityReleased = onEntityReleased;_cachedDestroyEntity = onDestroyEntity;// Add listener for updating lookupOnEntityCreated += (c, entity) => _entitiesLookup.Add(entity.creationIndex, (Entity)entity);OnEntityDestroyed += (c, entity) => _entitiesLookup.Remove(entity.creationIndex);}
}

代码虽然有点多,但是其实还是很清楚的,Context 中保存了的关键信息:

  • readonly Stack[] _componentPools; :component Pool数组,用来缓存component
  • HashSet _entities : 保存所有的entity
  • readonly Stack _reusableEntities : 保存回收的Entity,再次创建时,可以从这里取
  • readonly HashSet _retainedEntities : 保存还有引用的Entity,虽然被销毁了,但是还有引用,在DestroyAllEntities时会判断,如果有,证明之前有没有释放引用的情况。
  • readonly Dictionary<IMatcher, IGroup> _groups : 缓存了所有Matcher对应的Group,减少GC
  • readonly List[] _groupsForIndex :index表示每个组件的index,对应一个 List保存了引用这个组件的所有Group
  • Entity[] _entitiesCache : 外部访问_entities ,同时保证不会修改到_entities
  • int _creationIndex : 每个entity都有个唯一的creationIndex,在创建时指定

然后分析一下构造函数:
参数:

  • component的总数totalComponents,
  • entity标识index起始值startCreationIndex,
  • 上下文的说明信息contextInfo,
  • 以及自动entity引用计数工厂aercFactory,

然后构造函数对之前的变量进行初始化,以及对一些委托初始化,之后就可以根据Context处理了。

7. System

主要的逻辑处理的地方,遍历一组具有相同组件的Entity,所有System 都继承了ISystem

namespace Entitas {/// This is the base interface for all systems./// It's not meant to be implemented.public interface ISystem {bool IsEnable();void SetEnable(bool value);}
}

然后可以看到ICleanupSystem,IExecuteSystem,IInitializeSystem,IReactiveSystem,ITearDownSystem这些接口继承了ISystem :

然后有个Systems类,可以注册这些system:

using System.Collections.Generic;namespace Entitas {/// Systems provide a convenient way to group systems./// You can add IInitializeSystem, IExecuteSystem, ICleanupSystem,/// ITearDownSystem, ReactiveS/// ystem and other nested Systems instances./// All systems will be initialized and executed based on the order/// you added them.public class Systems : IInitializeSystem, IExecuteSystem, ICleanupSystem, ITearDownSystem {protected readonly List<IInitializeSystem> _initializeSystems;protected readonly List<IExecuteSystem> _executeSystems;protected readonly List<ICleanupSystem> _cleanupSystems;protected readonly List<ITearDownSystem> _tearDownSystems;protected bool m_IsEnable = true;/// Creates a new Systems instance.public Systems() {_initializeSystems = new List<IInitializeSystem>();_executeSystems = new List<IExecuteSystem>();_cleanupSystems = new List<ICleanupSystem>();_tearDownSystems = new List<ITearDownSystem>();}/// Adds the system instance to the systems list.public virtual Systems Add(ISystem system) {var initializeSystem = system as IInitializeSystem;if (initializeSystem != null) {_initializeSystems.Add(initializeSystem);}var executeSystem = system as IExecuteSystem;if (executeSystem != null) {_executeSystems.Add(executeSystem);}var cleanupSystem = system as ICleanupSystem;if (cleanupSystem != null) {_cleanupSystems.Add(cleanupSystem);}var tearDownSystem = system as ITearDownSystem;if (tearDownSystem != null) {_tearDownSystems.Add(tearDownSystem);}return this;}/// Calls Initialize() on all IInitializeSystem and other/// nested Systems instances in the order you added them.public virtual void Initialize() {for (int i = 0; i < _initializeSystems.Count; i++) {_initializeSystems[i].Initialize();}}/// Calls Execute() on all IExecuteSystem and other/// nested Systems instances in the order you added them.public virtual void Execute() {for (int i = 0; i < _executeSystems.Count; i++) {_executeSystems[i].Execute();}}/// Calls Cleanup() on all ICleanupSystem and other/// nested Systems instances in the order you added them.public virtual void Cleanup() {for (int i = 0; i < _cleanupSystems.Count; i++) {_cleanupSystems[i].Cleanup();}}/// Calls TearDown() on all ITearDownSystem  and other/// nested Systems instances in the order you added them.public virtual void TearDown() {for (int i = 0; i < _tearDownSystems.Count; i++) {_tearDownSystems[i].TearDown();}}/// Activates all ReactiveSystems in the systems list.public void ActivateReactiveSystems() {//...见源码}/// Deactivates all ReactiveSystems in the systems list./// This will also clear all ReactiveSystems./// This is useful when you want to soft-restart your application and/// want to reuse your existing system instances.public void DeactivateReactiveSystems() {//...见源码}/// Clears all ReactiveSystems in the systems list.public void ClearReactiveSystems() {//...见源码}public virtual bool IsEnable() { return m_IsEnable; }public virtual void SetEnable(bool value) { m_IsEnable = value; }}
}

三、为什么不用unity自带的ECS

  1. 这么多年了,unity的ECS还是preview版
  2. 我们ECS用于client以及server,因此逻辑核打算独立出来,不依赖unityEngine,且打算做竞技开房间类型的游戏,在服务器并不能依赖burst并发多线程去处理。

总结

这是一个系列文章,将逐步使用Entitas搭建整个游戏框架,当然对Entitas有部分改造,如去掉了TEntity泛型,全直接用Entity,后面将围绕ECS搭建整个游戏。

使用Entitas构建游戏框架(一)相关推荐

  1. Unity3D游戏引擎之构建游戏框架与导出IOS项目(一)

    Unity3D游戏引擎之构建游戏框架与导出IOS项目 雨松MOMO原创文章如转载,请注明:转载至我的独立域名博客雨松MOMO程序研究院,原文地址:http://www.xuanyusong.com/a ...

  2. 如何构建自己的游戏框架并且制作游戏

    这个教程就让我们学习怎么用这个游戏框架开发一个简单的空战游戏吧!由于素材有限,都是用的网上的素材.这个游戏可以改造成为空战或者植物大战僵尸等的养成类型游戏或者更多,原理都差不多.    一个出类拔萃的 ...

  3. Phaser开源2d引擎 javascript/html5游戏框架

    功能特点(Features) 易维护代码(Easy Asset Loading) Phaser可以加载图片,音频文件,数据文件,文本文件和自动解析精灵图和纹理地图集数据(出口纹理封隔器或Flash C ...

  4. Netty构建游戏服务器(一)--基本概念与原理

    一,Netty是什么 1,Netty是由 JBOSS 提供的一个 java开源 框架. 2,Netty是JAR包,一般使用ALL-IN-ONE的JAR包就可以开发了. 3,Netty不需要运行在Tom ...

  5. ECS 游戏框架背景知识

    1. 系统.实体.组件 组件是游戏数据的集合 [1] 实体是组件的集合 系统是方法的集合 因此: 一个实体的意义取决于其组件的组合 一个系统的意义取决于其方法集处理的组件集 例如: 一个实体是否拥有运 ...

  6. 介绍几个flash游戏框架

    转载请标明原文出处:http://chaimzane.iteye.com/blog/432127 1.Citrus 介绍:   Citrus 是一个由Actionscript 3.0 语言和 Box2 ...

  7. 《分布式虚拟现实系统(DVR)》(Yanlz+Unity+SteamVR+分布式+DVR+人工智能+边缘计算+人机交互+云游戏+框架编程+立钻哥哥+)

    <分布式虚拟现实系统(DVR)> <分布式虚拟现实系统(DVR)> 版本 作者 参与者 完成日期 备注 YanlzVR_DVR_V01_1.0 严立钻 2019.07.11 # ...

  8. Android游戏开发:游戏框架的搭建(1)

    通常情况下,游戏开发的基本框架中,一般包括以下模块: 窗口管理(Window management):该模块负责在Android平台上创建.运行.暂停.恢复游戏界面等功能. 输入模块(Input):该 ...

  9. 简单的Windows游戏-第1部分:游戏框架

    我已决定使用C#和WinForms创建一个简单的Windows游戏,从而得出一系列见解. 还有其他方法可以完成此任务,但我选择了使事情保持简单并演示如何制作游戏的方法. 更有经验的开发人员会注意到我的 ...

最新文章

  1. javascript 实现模拟滚动条,但不支持鼠标滚轮
  2. python绘制灰度图片直方图-opencv+python 统计及绘制直方图
  3. 第二百五十天 how can I 坚持
  4. 入门:现实世界中的推荐系统(术语、技术等)
  5. jquery中$each()
  6. php 获取当天到23 59,js 获取当天23点59分59秒 时间戳 (最简单的方法)
  7. Java基础篇之什么是本机方法
  8. 用WebView加载本地图片的方法
  9. centos7 安装教程
  10. 学术论文写作之引言(Introduction)怎么写
  11. 快学数据挖掘—数据探索—贡献度分析
  12. asp.net中调用javascript函数实现多功能日期控件示例
  13. 如何升级自己的思维,成为你想成为的自己? ----《少有人走的路》讀後感
  14. 转:走向自治:关于德鲁克的五个关键词
  15. Mysql序号 查询
  16. DataGridView 单击选中一整行,只能单选,不能选择多行,只能选择一行
  17. Windows下faceswap的安装
  18. 对XP系统中Autorun.inf Autorun.exe以及RECYCLER文件夹的认识
  19. [poj1741]tree 解题报告
  20. tvOS 开发第一个tvOS应用

热门文章

  1. 面试/笔试第一弹 —— 计算机网络面试问题集锦【转】
  2. 如何用U盘安装操作系统
  3. WIN7下WIFI共享上网
  4. 郑州73中学计算机老师,2019年关于“郑州市中学信息技术优质课评比”的通知
  5. 2021-03-13高级经理计算题:成本效益分析
  6. 使用易语言实现远程CALL调用
  7. 自己编一个大乐透选号器
  8. CSDN博客炫丽图标调整字体大小和颜色
  9. char和varchar的区别是什么?
  10. 安全合规/GDPR--15--通用数据保护条例-目录索引