使用UrhoSharp
来源:互联网 发布:maven 打包 java 工程 编辑:程序博客网 时间:2024/06/11 01:03
使用UrhoSharp
- PDF用于离线使用
- 下载PDF
- 互动:
- 行星地球工作簿
- 探索协调工作簿
让我们知道你对此的感受
UrhoSharp引擎概述
在您编写第一个游戏之前,您需要熟悉基础知识:如何设置场景,如何加载资源(包含您的作品)以及如何为游戏创建简单的交互。
场景,节点,组件和相机
场景模型可以描述为基于组件的场景图。场景由场景节点的层次结构组成,从根节点开始,也代表整个场景。每个Node
都有一个3D变换(位置,旋转和缩放),一个名称,一个ID,加上任意数量的组件。组件使节点生命,它们可以添加一个视觉表示(StaticModel
),它们可以发出声音(SoundSource
),它们可以提供碰撞边界等等。
您可以使用Urho编辑器创建场景和设置节点,也可以从C#代码执行操作。在本文档中,我们将探讨使用代码设置的东西,因为它们说明了让屏幕显示所需的元素
除了设置你的场景,你需要设置一个Camera
,这是什么决定了将向用户显示什么。
设置你的场景
通常您可以在Start方法中创建此窗体:
var scene = new Scene ();// Create the Octree component to the scene. This is required before// adding any drawable components, or else nothing will show up. The// default octree volume will be from -1000, -1000, -1000) to//(1000, 1000, 1000) in world coordinates; it is also legal to place// objects outside the volume but their visibility can then not be// checked in a hierarchically optimizing mannerscene.CreateComponent<Octree> ();// Create a child scene node (at world origin) and a StaticModel// component into it. Set the StaticModel to show a simple plane mesh// with a "stone" material. Note that naming the scene nodes is// optional. Scale the scene node larger (100 x 100 world units)var planeNode = scene.CreateChild("Plane");planeNode.Scale = new Vector3 (100, 1, 100);var planeObject = planeNode.CreateComponent<StaticModel> ();planeObject.Model = ResourceCache.GetModel ("Models/Plane.mdl");planeObject.SetMaterial(ResourceCache.GetMaterial("Materials/StoneTiled.xml"));
组件
渲染3D对象,声音播放,物理和脚本化逻辑更新都可以通过调用在节点中创建不同的组件来启用CreateComponent<T>()
。例如,如下设置您的节点和光分量:
// Create a directional light to the world so that we can see something. The// light scene node's orientation controls the light direction; we will use// the SetDirection() function which calculates the orientation from a forward// direction vector.// The light will use default settings (white light, no shadows)var lightNode = scene.CreateChild("DirectionalLight");lightNode.SetDirection (new Vector3(0.6f, -1.0f, 0.8f));
我们创建了一个名为“ DirectionalLight
” 的节点上方,并为其设置了一个方向,但没有别的。现在,我们可以通过将Light
组件附加到它来将上述节点转换成发光节点CreateComponent
:
var light = lightNode.CreateComponent<Light>();
创建到Scene
本身的组件具有特殊的作用:实现场景范围的功能。应在所有其他组件之前创建它们,并包括以下内容:
Octree
:实现空间分区和加速可见性查询。没有这个3D对象无法渲染。PhysicsWorld
:实现物理模拟。物理组件如无法正常工作RigidBody
或CollisionShape
无法正常工作。DebugRenderer
:实现调试几何渲染。
普通组件喜欢 Light
, Camera
或 StaticModel
不应该直接创建进 Scene
,而是成子节点。
图书馆提供了各种各样的组件,您可以将其附加到节点,使其生活:用户可见元素(模型),声音,刚体,碰撞形状,相机,光源,粒子发射器等等。
形状
为了方便,Urho.Shapes命名空间中可以使用各种形状作为简单的节点。这些包括盒子,球体,圆锥体,圆柱体和飞机。
相机和视口
就像光,摄像头是组件,所以你需要将组件附加到一个节点,这样做是这样的:
var CameraNode = scene.CreateChild ("camera");camera = CameraNode.CreateComponent<Camera>();CameraNode.Position = new Vector3 (0, 5, 0);
有了这个,你已经创建了一个摄像头,并且将相机放置在3D世界中,下一步是通知Application
这是您要使用的相机,这是通过以下代码完成的:
Renderer.SetViewPort (0, new Viewport (Context, scene, camera, null))
现在你应该可以看到你的创作结果了。
识别和场景层次
不同于节点,组件没有名称; 同一个节点内的组件只能通过它们的类型和节点的组件列表中的索引来标识,该列表以创建顺序填充,例如,您可以Light
从lightNode
上面的对象中检索 组件,如下所示:
var myLight = lightNode.GetComponent<Light>();
您还可以通过检索Components
返回IList<Component>
可以使用的属性来获取所有组件的列表 。
创建时,节点和组件都将获取场景全局整数ID。他们可以从场景使用功能来查询 GetNode(uint id)
和GetComponent(uint id)
。这比例如执行递归的基于名称的场景节点查询快得多。
没有一个实体或游戏对象的内置概念; 而是由程序员来决定节点层次结构,在哪些节点放置任何脚本逻辑。通常,3D世界中的自由移动对象将被创建为根节点的子节点。可以使用或不使用名称来创建节点 CreateChild()
。不强制执行节点名称的唯一性。
每当有一些分层组合时,建议(并且实际上是必要的,因为组件没有自己的3D变换)来创建子节点。
例如,如果一个角色持有一个对象在他的手中,对象应该有自己的节点,它将被父母的角色的手骨(也是a Node
)。例外的是物理学 CollisionShape
,它可以相对于节点单独地偏移和旋转。
注意,Scene
当计算子节点的世界派生变换时,自己的变换被有目的地忽略为优化,所以改变它没有任何效果,它应该保持原样(原点位置,不旋转,无缩放)。
Scene
节点可以自由地重新定位。相反,组件总是属于它们附加到的节点,并且不能在节点之间移动。这两个节点和组件都提供了一个Remove()
完成此功能的功能,而无需通过父节点。一旦节点被移除,调用该函数后,所讨论的节点或组件上的操作就不安全。
也可以创建Node
不属于场景的。这对于摄像机在可能被加载或保存的场景中移动是有用的,因为相机不会与实际场景一起保存,并且在场景加载时不会被破坏。但是,请注意,将几何,物理或脚本组件创建到未附加的节点,然后将其移动到某个场景中将导致这些组件无法正常工作。
场景更新
更新启用的场景(默认)将在每个主循环迭代中自动更新。应用程序的SceneUpdate
事件处理程序被调用。
节点和组件可以通过禁用它们从场景更新中排除Enabled
。行为取决于特定的组件,但是例如禁用可绘制组件也使其不可见,同时禁用声源组件将其静音。如果一个节点被禁用,它的所有组件都将被视为禁用,无论其自己的启用/禁用状态如何。
向您的组件添加行为
构建游戏的最佳方式是使自己的组件在游戏中封装一个演员或元素。这使得功能自包含,从用于显示它的资产到其行为。
向组件添加行为的最简单的方法是使用操作,这些操作是可以将其与C#异步编程进行排队和组合的指令。这使您的组件的行为达到非常高的水平,并使其更容易了解发生了什么。
或者,您可以通过更新每个框架上的组件属性来准确控制组件发生的情况(在“基于帧的行为”部分中讨论)。
操作
您可以使用Actions轻松添加行为。动作可以改变各种节点属性并在一段时间内执行,或者通过给定的动画曲线重复它们多次。
例如,考虑一个“云”节点在你的场景,你可以像这样淡化:
await cloud.RunActionsAsync (new FadeOut (duration: 3))
动作是不可变的对象,它允许您重用驱动不同对象的动作。
一个常见的成语是创建一个执行相反操作的动作:
var gotoExit = new MoveTo (duration: 3, position: exitLocation);var return = gotoExit.Reverse ();
以下示例将在三秒钟内为您褪色您的对象。您也可以依次运行一个操作,例如,您可以先移动云,然后隐藏它:
await cloud.RunActionsAsync ( new MoveBy (duration: 1.5f, position: new Vector3(0, 0, 15), new FadeOut (duration: 3));
如果您希望同时进行两个操作,您可以使用并行操作,并提供并行执行的所有操作:
await cloud.RunActionsAsync ( new Parallel ( new MoveBy (duration: 3, position: new Vector3(0, 0, 15), new FadeOut (duration: 3)));
在上面的例子中,云将同时移动和淡出。
您会注意到这些使用C#等待,这使您能够线性地考虑您想要实现的行为。
基本动作
这些是UrhoSharp支持的操作:
- 移动节点:
MoveTo
,MoveBy
,Place
,BezierTo
,BezierBy
,JumpTo
,JumpBy
- 旋转节点:
RotateTo
,RotateBy
- 缩放节点:
ScaleTo
,ScaleBy
- 衰落节点:
FadeIn
,FadeTo
,FadeOut
,Hide
,Blink
- 着色:
TintTo
,TintBy
- 瞬间:
Hide
,Show
,Place
,RemoveSelf
,ToggleVisibility
- 循环:
Repeat
,RepeatForever
,ReverseTime
其他先进功能包括的组合Spawn
和Sequence
动作。
缓解 - 控制您的行动速度
缓解是指导动画展现的方式,它可以让你的动画更愉快。默认情况下,您的操作将具有线性行为,例如,MoveTo
操作将具有非常机器人的运动。您可以将“动作”包含在缓解动作中以改变行为,例如,缓慢启动运动,加速并缓慢到达结束(EasyInOut
)的动作。
您可以通过将现有的Action包装到缓解操作中来实现,例如:
await cloud.RunActionAsync ( new EaseInOut ( new MoveTo (duration: 3, position: new Vector (0,0,15)), rate:1))
有许多宽松模式,下图显示了各种宽松类型及其在一段时间内从头到尾控制的对象的价值的行为:
使用操作和异步代码
在您的Component
子类中,您应该引入一种异步方法,以准备组件行为并为其驱动其功能。然后,您将使用await
程序的其他部分的C#关键字,您的Application.Start
方法或响应于应用程序中的用户或故事点来调用此方法。
例如:
class Robot : Component { public bool IsAlive; async void Launch () { // Dress up our robot var cache = Application.ResourceCache; var model = node.CreateComponent<StaticModel>(); model.Model = cache.GetModel ("robot.mdl")); model.SetMaterial (cache.GetMaterial ("robot.xml")); Node.SetScale (1); // Bring the robot into our scene await Node.RunActionsAsync( new MoveBy(duration: 0.6f, position: new Vector3(0, -2, 0))); // Make him move around to avoid the user fire MoveRandomly(minX: 1, maxX: 2, minY: -3, maxY: 3, duration: 1.5f); // And simultaneously have him shoot at the user StartShooting(); } protected async void MoveRandomly (float minX, float maxX, float minY, float maxY, float duration) { while (IsAlive){ var moveAction = new MoveBy(duration, new Vector3(RandomHelper.NextRandom(minX, maxX), RandomHelper.NextRandom(minY, maxY), 0)); await Node.RunActionsAsync(moveAction, moveAction.Reverse()); } } protected async void StartShooting() { while (IsAlive && Node.Components.Count > 0){ foreach (var weapon in Node.Components.OfType<Weapon>()){ await weapon.FireAsync(false); if (!IsAlive) return; } await Node.RunActionsAsync(new DelayTime(0.1f)); } }}
在上述Launch
方法中,启动三个操作:机器人进入场景,此操作将在0.6秒的时间内更改节点的位置。由于这是一个异步选项,这将作为下一个调用的指令同时发生 MoveRandomly
。该方法将机器人的位置并行改变为随机位置。这是通过执行两个复合动作,移动到新位置,并返回到原始位置并重复此操作,只要机器人保持活动即可。为了使事情更有趣,机器人将同时保持拍摄。拍摄只会每0.1秒钟开始一次。
基于帧的行为规划
如果要以逐帧为基础来控制组件的行为,而不是使用动作,那么您要做的就是重写子类的OnUpdate
方法Component
。此方法每帧调用一次,只有将ReceiveSceneUpdates属性设置为true时才调用此方法。
以下内容显示了如何创建一个Rotator
组件,然后将其连接到节点,从而导致节点旋转:
class Rotator : Component { public Rotator() { ReceiveSceneUpdates = true; } public Vector3 RotationSpeed { get; set; } protected override void OnUpdate(float timeStep) { Node.Rotate(new Quaternion( RotationSpeed.X * timeStep, RotationSpeed.Y * timeStep, RotationSpeed.Z * timeStep), TransformSpace.Local); }}
这是你将如何将这个组件附加到一个节点:
Node boxNode = new Node();var rotator = new Rotator() { RotationSpeed = rotationSpeed };boxNode.AddComponent (rotator);
组合样式
您可以使用基于异步/动作的模型编程大量的行为,这对于编程的启发风格非常有用,但您也可以调整组件的行为,并在每个框架上运行一些更新代码。
例如,在SamplyGame演示中,这是在Enemy
类编码中使用的基本行为使用动作,但它还通过设置节点的方向来确保组件指向用户 Node.LookAt
:
protected override void OnUpdate(SceneUpdateEventArgs args) { Node.LookAt( new Vector3(0, -3, 0), new Vector3(0, 1, -1), TransformSpace.World); base.OnUpdate(args); }
加载和保存场景
场景可以加载和保存为XML格式; 看到功能 LoadXml
和SaveXML()
。加载场景时,首先删除其中的所有现有内容(子节点和组件)。使用Temporary
属性标记为临时的节点和组件将不会被保存。串行器处理所有内置组件和属性,但它不够聪明,不能处理您的Component子类中定义的定制属性和字段。但是它为此提供了两种虚拟方法:
OnSerialize
您可以在其中注册您的序列化的自定义状态OnDeserialized
您可以在其中获取保存的自定义状态。
通常,自定义组件将如下所示:
class MyComponent : Component { // Constructor needed for deserialization public MyComponent(IntPtr handle) : base(handle) { } public MyComponent() { } // user defined properties (managed state): public Quaternion MyRotation { get; set; } public string MyName { get; set; } public override void OnSerialize(IComponentSerializer ser) { // register our properties with their names as keys using nameof() ser.Serialize(nameof(MyRotation), MyRotation); ser.Serialize(nameof(MyName), MyName); } public override void OnDeserialize(IComponentDeserializer des) { MyRotation = des.Deserialize<Quaternion>(nameof(MyRotation)); MyName = des.Deserialize<string>(nameof(MyName)); } // called when the component is attached to some node public override void OnAttachedToNode() { var node = this.Node; }}
对象预制
加载或保存整个场景对于需要动态创建新对象的游戏来说,不够灵活。另一方面,创建复杂对象并在代码中设置属性也将是乏味的。因此,也可以保存将包括其子节点,组件和属性的场景节点。这些可以随后方便地作为一组加载。这样一个保存的对象通常被称为预制。有三种方法可以做到这一点:
- 在代码中通过调用
Node.SaveXml
Node - 在编辑器中,通过选择层次结构窗口中的节点,并从“文件”菜单中选择“将节点另存为”。
- 使用“节点”命令
AssetImporter
,将保存场景节点层次结构和输入资源中包含的任何模型(例如Collada文件)
要将保存的节点实例化为场景,请调用InstantiateXml()
。该节点将被创建为场景的孩子,但可以在此之后自由地重新定位。需要指定放置节点的位置和旋转。以下代码演示了如何将预制件实例化为Ninja.xm
具有所需位置和旋转的场景:
var prefabPath = Path.Combine (FileSystem.ProgramDir,"Data/Objects/Ninja.xml");using (var file = new File(Context, prefabPath, FileMode.Read)){ scene.InstantiateXml(file, desiredPos, desiredRotation, CreateMode.Replicated);}
活动
UrhoObjects引发了一些事件,这些事件在生成它们的各种类中表现为C#事件。除了基于C#的事件模型之外,还可以使用SubscribeToXXX
允许您订阅并保留订阅令牌的方法,您可以稍后使用它们取消订阅。不同的是,前者将允许许多呼叫者订阅,而第二个只允许一个,但允许使用更好的lambda风格的方法,但是,允许容易地删除订阅。它们是相互排斥的。
订阅一个事件时,您必须提供一个方法,该方法使用适当的事件参数接受参数。
例如,这是您如何订阅鼠标按钮事件:
public void override Start (){ UI.MouseButtonDown += HandleMouseButtonDown;}void HandleMouseButtonDown(MouseButtonDownEventArgs args){ Console.WriteLine ("button pressed");}
使用lambda风格:
public void override Start (){ UI.MouseButtonDown += args => { Console.WriteLine ("button pressed"); };}
有时你会想要停止接收事件的通知,在这种情况下,将调用返回值保存到SubscribeTo
方法中,并调用取消订阅方法:
Subscription mouseSub;public void override Start (){ mouseSub = UI.SubscribeToMouseButtonDown (args => { Console.WriteLine ("button pressed"); mouseSub.Unsubscribe (); };}
事件处理程序接收的参数是一个强类型的事件参数类,它将针对每个事件并包含事件有效负载。
响应用户输入
您可以订阅各种事件,例如通过订阅该事件的按键,并响应正在传递的输入:
Start (){ UI.KeyDown += HandleKeyDown;}void HandleKeyDown (KeyDownEventArgs arg){ switch (arg.Key){ case Key.Esc: Engine.Exit (); return;}
但是在许多情况下,您希望现场更新处理程序在更新密钥时检查当前的状态,并相应地更新代码。例如,以下可用于根据键盘输入更新相机位置:
protected override void OnUpdate(float timeStep){ Input input = Input; // Movement speed as world units per second const float moveSpeed = 4.0f; // Read WASD keys and move the camera scene node to the // corresponding direction if they are pressed if (input.GetKeyDown(Key.W)) CameraNode.Translate(Vector3.UnitY * moveSpeed * timeStep, TransformSpace.Local); if (input.GetKeyDown(Key.S)) CameraNode.Translate(new Vector3(0.0f, -1.0f, 0.0f) * moveSpeed * timeStep, TransformSpace.Local); if (input.GetKeyDown(Key.A)) CameraNode.Translate(new Vector3(-1.0f, 0.0f, 0.0f) * moveSpeed * timeStep, TransformSpace.Local); if (input.GetKeyDown(Key.D)) CameraNode.Translate(Vector3.UnitX * moveSpeed * timeStep, TransformSpace.Local);}
资源(资产)
资源包括在初始化或运行时从大容量存储加载的UrhoSharp中的大部分内容:
Animation
- 用于骨骼动画Image
- 表示以各种图形格式存储的图像Model
- 3D模型Material
- 用于渲染模型的材料。ParticleEffect
- 描述粒子发射器的工作原理,参见下面的“ 粒子 ”。Shader
- 定制着色器Sound
- 播放声音,请参阅下面的“ 声音 ”。Technique
- 材料渲染技术Texture2D
- 2D纹理Texture3D
- 3D纹理TextureCube
- 立方体纹理XmlFile
它们由ResourceCache
子系统(可用Application.ResourceCache
)管理和加载。
相对于注册的资源目录或包文件,资源本身由其文件路径标识。默认情况下,引擎注册资源目录Data
和CoreData
,或者其包装Data.pak
和CoreData.pak
是否存在。
如果加载资源失败,将会记录错误并返回null引用。
以下示例显示从资源缓存获取资源的典型方法。在这种情况下,UI元素的纹理使用类中的ResourceCache
属性Application
。
healthBar.SetTexture(ResourceCache.GetTexture2D("Textures/HealthBarBorder.png"));
也可以手动创建资源,并将其存储到资源缓存中,就像它们已经从磁盘加载一样。
可以根据资源类型设置内存预算:如果资源占用的内存大于允许的内存,那么如果不再使用,最早的资源将从高速缓存中删除。默认情况下,内存预算设置为无限制。
带来3D模型和图像
Urho3D尽可能地尝试使用现有的文件格式,并且只有在绝对需要时才定义自定义文件格式,如模型(.mdl)和动画(.ani)。对于这些类型的资产,Urho提供了一个转换器 - AssetImporter ,它可以消耗许多流行的3D格式,如fbx,dae,3ds和obj等。
还有一个方便的Blender https://github.com/reattiva/Urho3D-Blender加载项 ,可以以适合Urho3D的格式导出Blender资产。
后台加载资源
通常情况下,请求使用的一个资源时ResourceCache
的 Get
方法,立即将它们在主线程,这可能需要几毫秒为所有必要的步骤(加载文件从磁盘,解析数据,上传到GPU如有必要)加载,并且因此导致帧速率下降。
如果你事先知道你需要什么资源,可以通过调用来请求他们在后台线程中加载 BackgroundLoadResource()
。您可以使用该SubscribeToResourceBackgroundLoaded
方法订阅Resource Background Loaded事件 。它会判断装载是否成功或失败。根据资源,只有一部分加载过程可能被移动到后台线程,例如整理GPU上传步骤总是需要在主线程中发生。请注意,如果为为后台加载排队的资源调用资源加载方法之一,主线程将停止,直到其加载完成。
异步场景加载功能,LoadAsync()
并且 LoadAsyncXML()
可以先选择后台加载资源,然后再继续加载场景内容。它也可以用于仅通过指定来加载资源而不修改场景 LoadMode.ResourcesOnly
。这样可以准备一个场景或对象预制文件,以便快速实例化。
最后,完成后台加载资源的每个帧花费的最大时间(以毫秒为单位)可以通过设置该FinishBackgroundResourcesMs
属性进行配置 ResourceCache
。
声音
声音是游戏的重要组成部分,UrhoSharp框架提供了一种在游戏中播放声音的方法。通过将SoundSource
组件附加到 Node
然后从您的资源播放命名文件来播放声音 。
这是如何做的:
var explosionNode = Scene.CreateChild();var sound = explosionNode.CreateComponent<SoundSource>();soundSource.Play(Application.ResourceCache.GetSound("Sounds/ding.wav"));soundSource.Gain = 0.5f;soundSource.AutoRemove = true;
粒子
粒子提供了一种简单的方法,为您的应用程序添加一些简单而便宜的效果。您可以使用像http://onebyonedesign.com/flash/particleeditor/这样的工具来使用以PEX格式存储的粒子 。
粒子是可以添加到节点的组件。您需要调用节点的CreateComponent<ParticleEmitter2D>
方法来创建粒子,然后通过将Effect属性设置为从资源缓存加载的2D效果来配置粒子。
例如,您可以在组件上调用此方法,以显示一些在其命中时呈现为爆炸的粒子:
public async void Explode (Component target){ // show a small explosion when the missile reaches an aircraft. var cache = Application.ResourceCache; var explosionNode = Scene.CreateChild(); explosionNode.Position = target.Node.WorldPosition; explosionNode.SetScale(1f); var particle = explosionNode.CreateComponent<ParticleEmitter2D>(); particle.Effect = cache.GetParticleEffect2D("explosion.pex"); var scaleAction = new ScaleTo(0.5f, 0f); await explosionNode.RunActionsAsync( scaleAction, new DelayTime(0.5f)); explosionNode.Remove();}
上面的代码将创建一个连接到当前组件的爆炸节点,在这个爆炸节点内,我们创建一个2D粒子发射器,并通过设置Effect属性进行配置。我们运行两个动作,一个将该节点缩小的动作,另一个动作将该节点缩小为0.5秒。然后我们去除爆炸,这也从屏幕上消除了粒子的影响。
当使用球体纹理时,上述粒子就像这样呈现:
如果您使用块状纹理,这是什么?
多线程支持
UrhoSharp是一个单线程库。这意味着您不应该尝试从后台线程调用UrhoSharp中的方法,否则可能会损坏应用程序的状态,并可能导致应用程序崩溃。
如果要在后台运行一些代码,然后在主UI上更新Urho组件,可以使用该 Application.InvokeOnMain(Action)
方法。此外,您可以使用C#等待和.NET任务API来确保代码在正确的线程上执行。
UrhoEditor
您可以从Urho网站下载您的平台的Urho编辑器,转到下载并选择最新版本。
- 使用UrhoSharp
- UrhoSharp简介
- UrhoSharp Android入门
- UrhoSharp WPF 开发入门
- UrhoSharp Xamarin.Forms 开发入门
- UrhoSharp iOS和tvOS 开发入门
- 使用
- 使用
- 使用
- 使用
- 使用
- 使用
- 使用++,--
- 使用$@ $!
- 使用
- SoftICE使用(指令使用)
- 使用GraphEdit使用
- 使用HtmlParser使用心得
- 设计模式之建造者模式
- bzoj3196 Tyvj 1730 二逼平衡树
- Android中MVP模式的实例
- vue2+webpack2 初始化项目
- SpringMVC学习之JSTL条件行为和遍历行为
- 使用UrhoSharp
- java多线程
- Hadoop的理论基础来自谷歌的三大论文,以下是三大论文的中文版
- Vue.js进行查询操作
- 使用YCSB测试MongoDB的微分片性能
- 神经网络入门(三)
- iOS获取网络时间,网络获取时间,也就是现实中的时间
- 项目登陆功能的总体架设
- Python之创建tuple