Unity物理系统自定义实现:从欧拉积分到碰撞响应
1. 项目概述为什么我们要“手写”物理系统在Unity开发圈子里提到物理系统大家的第一反应往往是内置的NVIDIA PhysX。它功能强大、开箱即用一个Rigidbody组件加上碰撞器就能让物体遵循牛顿定律运动。这确实是快速原型和大多数游戏项目的首选。但作为一名有追求的开发者尤其是当你开始接触性能要求苛刻的移动平台、需要实现特定风格化物理效果比如《人类一败涂地》那种软绵绵的布娃娃或者在做一些严肃的仿真项目时你可能会开始对那个“黑盒”感到不安。你不知道它每一帧具体算了什么出了奇怪的穿模或抖动时排查起来像在猜谜更别提在某些极端情况下PhysX的表现可能成为性能瓶颈。“手写物理系统”听起来像是个庞大得吓人的工程仿佛要从头推导牛顿三定律。但实际上它远没有那么遥不可及。这里的“手写”更准确地说是“自定义实现核心物理模拟循环”。我们并非要替代整个PhysX而是在理解其基本原理的基础上针对特定需求构建一个更轻量、更可控、更符合项目预期的物理模拟模块。这就像你虽然会开车但学习发动机原理和简单维修能让你在车出小毛病时不至于束手无策甚至能自己动手改装提升性能。从网络热词来看大家关心的问题非常实际从“unity性能优化”、“unity打包安卓”到具体的“unity 实现完全弹性碰撞”都指向了对底层控制力和性能的渴求。当你需要精细控制碰撞响应、实现非标准的物理行为比如基于距离的引力、自定义的流体阻力或者仅仅是为了教学与深入理解手写一个简化但完整的物理循环就变得非常有价值。它能让你彻底摆脱“魔法”般的组件对游戏世界中每一个物体的运动了如指掌。2. 核心思路从“组件驱动”到“数据驱动”的思维转变Unity内置物理系统是典型的“组件驱动”模式。你挂载组件引擎在背后运行一个复杂的、多线程的物理引擎你通过回调如OnCollisionEnter获取结果。而手写物理系统则需要我们切换到“数据驱动”和“过程可控”的思维模式。2.1 物理模拟的基本循环一个最简单的、基于欧拉积分的物理模拟循环其核心步骤可以概括为以下几个阶段我们将完全自己来控制收集状态我们需要为场景中每一个需要模拟的物体维护一套完整的状态数据。这至少包括位置Vector3 position、旋转Quaternion rotation、线速度Vector3 velocity、角速度Vector3 angularVelocity。在Unity中我们通常会用一个自定义的PhysicsBody类或结构体来封装这些数据而不是依赖Transform和Rigidbody。计算合力这是物理的“原因”阶段。针对每个物体遍历所有作用在其上的力。这包括恒定力如重力 (mass * gravity)。外力如玩家施加的推力、爆炸冲击力。约束力如弹簧力、关节力。阻尼力模拟空气阻力、摩擦力通常与速度方向相反 (-damping * velocity)。 将所有力矢量相加得到物体在当前时刻所受的合力Vector3 totalForce。积分更新这是物理的“结果”阶段。根据牛顿第二定律F m * a我们可以由合力计算出加速度a totalForce / mass。然后通过积分将加速度转化为速度的变化再将速度转化为位置的变化。最基础的显式欧拉积分代码如下// 假设 Time.fixedDeltaTime 是我们的固定时间步长 deltaTime float deltaTime Time.fixedDeltaTime; // 计算加速度 Vector3 acceleration totalForce / mass; // 积分速度v_new v_old a * dt velocity acceleration * deltaTime; // 积分位置p_new p_old v_new * dt position velocity * deltaTime; // 对于旋转类似地处理角加速度和角速度 // angularVelocity totalTorque / inertiaTensor * deltaTime; // 使用角速度更新旋转这里涉及四元数积分稍复杂这就是运动模拟最核心的一行代码。高级的积分器如Verlet, RK4会更复杂、更稳定但欧拉法足以让我们入门并理解整个过程。碰撞检测与响应在更新位置后我们需要检查物体之间或物体与环境之间是否发生了穿插。如果发生了碰撞就需要计算碰撞点、法线并根据物理定律如动量守恒、能量守恒或自定义规则修正物体的速度和位置这就是碰撞响应。这是手写物理中最具挑战性的部分之一。同步渲染状态最后将我们计算出的新position和rotation赋值给GameObject的Transform组件让视觉表现与我们的物理模拟状态同步。2.2 与Unity内置系统的关键区别理解了基本循环我们就能看清手写系统与内置系统的几个关键区别确定性内置的PhysX为了性能和稳定性在某些情况下特别是多线程可能引入非确定性。而我们自己写的单线程欧拉积分在相同的初始状态和输入下每次运行的结果是完全一致的这对网络同步、录像回放等功能至关重要。性能可控性你可以精确控制模拟的复杂度。如果场景中只有100个需要物理模拟的球你就只算这100个。不会有多余的 overhead。在移动端这可能是性能救星。功能定制化你想让重力方向随时间正弦变化想实现一个只有吸引力没有排斥力的“引力场”在自定义的合力计算阶段你可以轻松加入任何天马行力的公式不受任何引擎限制。调试透明度所有数据都在你的掌握之中。物体为什么飞出去了你可以逐帧打印出力、速度、加速度像调试普通业务逻辑一样调试物理问题无处遁形。注意手写系统并非万能。对于复杂的连续碰撞检测CCD、凹凸网格MeshCollider之间的精确碰撞、或者包含大量刚体和复杂约束的场景成熟的物理引擎经过数十年优化在稳定性和性能上依然具有绝对优势。我们的目标不是取代而是在特定领域获得超越引擎的灵活性与控制力。3. 实践第一步构建一个最小化的自定义刚体理论说得再多不如动手写一行代码。让我们从创建一个最简单的、只受重力作用的球体开始。3.1 定义物理体数据类首先我们创建一个SimplePhysicsBody类用来存储物体的物理状态。这个类不继承MonoBehaviour它是一个纯粹的数据容器。using UnityEngine; [System.Serializable] public class SimplePhysicsBody { // 公共字段便于在Inspector中调试和调整 public float mass 1.0f; public float drag 0.1f; // 线性阻尼模拟空气阻力 public float angularDrag 0.05f; // 角阻尼 public Vector3 velocity; public Vector3 angularVelocity; public bool useGravity true; // 内部状态通常由物理系统管理 [System.NonSerialized] public Vector3 position; [System.NonSerialized] public Quaternion rotation; [System.NonSerialized] public Vector3 force; // 累积的力 // 初始化方法 public void Initialize(Transform transform) { position transform.position; rotation transform.rotation; velocity Vector3.zero; angularVelocity Vector3.zero; force Vector3.zero; } }为什么要把位置、旋转从Transform里“剥离”出来因为我们要将物理模拟和渲染更新解耦。物理模拟在一个固定的时间步长如FixedUpdate中运行使用我们自己的position和rotation变量进行计算。计算完成后再将结果“应用”到Transform上。这避免了直接修改Transform可能带来的帧率依赖问题也是实现确定性模拟的基础。3.2 实现一个简单的物理管理器接下来我们需要一个管理器来驱动所有SimplePhysicsBody的模拟。这个管理器继承MonoBehaviour并在FixedUpdate中运行我们的物理循环。using UnityEngine; using System.Collections.Generic; public class SimplePhysicsWorld : MonoBehaviour { public static SimplePhysicsWorld Instance; // 简单单例方便访问 public Vector3 gravity new Vector3(0, -9.81f, 0); // 可自定义的重力 public ListSimplePhysicsBody bodies new ListSimplePhysicsBody(); private void Awake() { if (Instance null) Instance this; } private void FixedUpdate() { float deltaTime Time.fixedDeltaTime; // 1. 清除上一帧累积的力为计算新合力做准备 foreach (var body in bodies) { body.force Vector3.zero; } // 2. 计算并累积每个物体受到的合力 foreach (var body in bodies) { // 添加重力 if (body.useGravity) { body.force body.mass * gravity; // F m * g } // 添加阻尼力与速度方向相反 body.force -body.drag * body.velocity; // 这里可以添加其他力如风力、推力等 // body.force CustomForce(body); } // 3. 积分根据合力更新速度和位置 foreach (var body in bodies) { // 计算加速度 a F / m Vector3 acceleration body.force / body.mass; // 欧拉积分更新速度 body.velocity acceleration * deltaTime; // 欧拉积分更新位置 body.position body.velocity * deltaTime; // 处理旋转简化版假设不受扭矩 // 角速度阻尼 body.angularVelocity * (1f - body.angularDrag * deltaTime); // 更新旋转将角速度转换为四元数增量这是一个近似 Quaternion deltaRotation Quaternion.Euler(body.angularVelocity * deltaTime); body.rotation deltaRotation * body.rotation; } // 4. 后续会加入碰撞检测与响应 // ResolveCollisions(); // 5. 将更新后的物理状态同步到渲染Transform SyncTransforms(); } private void SyncTransforms() { foreach (var body in bodies) { // 这里需要一种方式将body和其对应的GameObject关联起来 // 例如可以要求每个body持有一个对Transform的引用 // 为了简化我们假设有一个配套的MonoBehaviour来负责这个关联 } } public void RegisterBody(SimplePhysicsBody body) { if (!bodies.Contains(body)) { bodies.Add(body); } } public void UnregisterBody(SimplePhysicsBody body) { bodies.Remove(body); } }3.3 创建可交互的物理物体组件现在我们需要一个桥接组件将GameObject、Transform和我们自定义的SimplePhysicsBody连接起来。using UnityEngine; public class SimpleRigidbody : MonoBehaviour { public SimplePhysicsBody physicsBody new SimplePhysicsBody(); private void Start() { // 初始化物理体数据 physicsBody.Initialize(transform); // 向物理世界注册自己 SimplePhysicsWorld.Instance?.RegisterBody(physicsBody); } private void FixedUpdate() { // 这个组件的FixedUpdate不是用来计算物理的 // 而是用来将物理世界计算好的结果应用回自己的Transform。 // 注意这个应用操作应该在物理世界所有计算完成后进行。 // 更优的做法是物理世界计算完后统一调用所有物体的同步方法。 // 这里为了简单我们先这样写。最佳实践会在后面讨论。 if (SimplePhysicsWorld.Instance ! null) { // 等待物理世界计算完毕我们直接应用位置和旋转。 // 实际中物理世界会管理这个同步过程。 } } // 提供一个方法供物理世界管理器调用用于同步状态 public void SyncTransformFromPhysics() { transform.position physicsBody.position; transform.rotation physicsBody.rotation; } // 施加一个力到物理体上在世界坐标系 public void AddForce(Vector3 force) { physicsBody.force force; } private void OnDestroy() { SimplePhysicsWorld.Instance?.UnregisterBody(physicsBody); } }同时我们需要修改SimplePhysicsWorld的SyncTransforms方法让它遍历所有注册的SimpleRigidbody组件并调用同步方法。为此管理器最好直接管理SimpleRigidbody列表。实操心得一FixedUpdate的时序陷阱在上面的简化代码中SimpleRigidbody在FixedUpdate里尝试同步位置而SimplePhysicsWorld也在FixedUpdate里计算物理并同步。这会造成竞争状态谁先执行Unity不保证不同脚本FixedUpdate的执行顺序。正确的模式是所有物理计算必须在一个权威的地方完成然后统一应用结果。因此我们应该让SimplePhysicsWorld完全掌控循环并在循环的最后主动调用每个注册物体的SyncTransformFromPhysics方法。SimpleRigidbody组件本身的FixedUpdate方法应该为空或仅处理与物理计算无关的逻辑。修改后的SimplePhysicsWorld会持有ListSimpleRigidbody并在FixedUpdate末尾遍历这个列表进行同步。SimpleRigidbody的Start方法中向这个世界注册自己即可。这样就确保了“计算-同步”的原子性。4. 碰撞检测与响应的实现从理论到代码让物体动起来只是第一步让它们彼此交互、不发生穿透才是物理系统的灵魂。碰撞处理分为两步检测Detection和响应Response。4.1 基础碰撞检测球与球的相交测试我们从最简单的形状开始球体。两个球体是否碰撞取决于它们中心点的距离是否小于半径之和。public struct SphereCollider { public Vector3 center; public float radius; } public static class CollisionDetection { public static bool CheckSphereSphere(SphereCollider a, SphereCollider b, out Vector3 normal, out float penetrationDepth) { Vector3 delta b.center - a.center; float distance delta.magnitude; float radiusSum a.radius b.radius; if (distance radiusSum) { // 发生碰撞 normal delta.normalized; // 碰撞法线从A指向B penetrationDepth radiusSum - distance; // 穿透深度 return true; } normal Vector3.zero; penetrationDepth 0; return false; } }这个函数返回是否碰撞并输出关键的碰撞信息法线normal和穿透深度penetrationDepth。法线方向定义了碰撞的表面方向响应力将沿此方向作用。穿透深度告诉我们物体“嵌进去”了多少我们需要把它们推开这个距离。4.2 基础碰撞响应冲量法检测到碰撞后我们需要修正物体的运动状态使其看起来像是发生了真实的碰撞。最常用的方法是冲量法Impulse Method。它基于物理中的动量守恒和碰撞定律计算一个瞬间的力冲量直接改变物体的速度。对于两个刚体A和B的碰撞计算分离冲量j的公式如下简化版假设碰撞无摩擦且完全弹性j -(1 e) * (V_ab · N) / (1/m_a 1/m_b)其中e是恢复系数弹性系数0为完全非弹性1为完全弹性。V_ab (V_b - V_a)是B相对于A的速度。N是碰撞法线从A指向B。·表示点积。m_a,m_b是质量。然后将这个冲量分别应用到两个物体的速度上V_a_new V_a - (j / m_a) * N V_b_new V_b (j / m_b) * N这个公式会使得两个物体沿碰撞法线方向分离。同时我们还需要进行位置修正Penetration Resolution将两个物体沿着法线方向推开直到它们刚好接触避免下一帧因为仍有穿透而持续产生碰撞。public static void ResolveSphereCollision(SimplePhysicsBody bodyA, SimplePhysicsBody bodyB, Vector3 collisionNormal, float penetrationDepth, Vector3 contactPoint) { // 1. 位置修正将物体推开以避免穿透 float totalInverseMass 1.0f / bodyA.mass 1.0f / bodyB.mass; Vector3 correction collisionNormal * (penetrationDepth / totalInverseMass); bodyA.position - correction / bodyA.mass; bodyB.position correction / bodyB.mass; // 2. 速度修正计算并应用冲量 float restitution 0.8f; // 恢复系数0.8表示80%的弹性 // 计算相对速度 Vector3 relativeVelocity bodyB.velocity - bodyA.velocity; // 计算沿法线方向的相对速度 float velocityAlongNormal Vector3.Dot(relativeVelocity, collisionNormal); // 如果物体正在分离则不应用冲量避免“粘滞” if (velocityAlongNormal 0) return; // 计算冲量标量 j float j -(1 restitution) * velocityAlongNormal; j / totalInverseMass; // 应用冲量 Vector3 impulse j * collisionNormal; bodyA.velocity - impulse / bodyA.mass; bodyB.velocity impulse / bodyB.mass; }4.3 集成到物理世界循环中现在我们需要在SimplePhysicsWorld的FixedUpdate循环中加入碰撞处理步骤。一个简单但低效的方法是每帧对所有物体进行两两检测O(n²)复杂度。对于学习和小规模场景这是可以接受的。在SimplePhysicsWorld.FixedUpdate()的积分步骤之后加入// 4. 简单的两两碰撞检测与响应性能较差仅用于演示 for (int i 0; i bodies.Count; i) { for (int j i 1; j bodies.Count; j) { // 假设每个物体都关联了一个SphereCollider SphereCollider colliderA GetColliderForBody(bodies[i]); // 需要你实现这个方法 SphereCollider colliderB GetColliderForBody(bodies[j]); if (CollisionDetection.CheckSphereSphere(colliderA, colliderB, out Vector3 normal, out float depth)) { ResolveSphereCollision(bodies[i], bodies[j], normal, depth, (colliderA.center colliderB.center) * 0.5f); } } }实操心得二迭代顺序与稳定性在位置修正时如果我们同时移动两个物体修正量是均摊的。但有时一个物体是“静态”的质量无限大我们通常只移动动态物体。这需要在ResolveSphereCollision中根据质量进行判断。此外当一帧内发生多次碰撞时处理的顺序会影响最终结果。更高级的物理引擎会使用迭代求解器如Sequential Impulses多次处理碰撞以获得更稳定、更真实的结果。我们的简单实现可能会在复杂碰撞堆叠时出现抖动或溢出这是需要意识到的局限性。5. 性能优化与高级话题入门当物体数量增多时两两检测的O(n²)复杂度将无法承受。此时必须引入空间划分算法。5.1 空间划分四叉树/八叉树与网格网格Grid将世界划分为均匀的单元格。每个物体根据其位置放入一个或多个单元格中。检测时只需检查同一单元格及相邻单元格内的物体。实现简单适用于物体分布相对均匀的场景。四叉树2D/八叉树3D递归地将空间划分为四个或八个子区域直到每个区域内的物体数量低于某个阈值。它能自适应地处理物体稀疏或密集的区域是更通用的选择。实现一个简单的网格空间划分public class SpatialGrid { public float cellSize; private DictionaryVector3Int, ListSimplePhysicsBody grid new DictionaryVector3Int, ListSimplePhysicsBody(); public Vector3Int GetCellKey(Vector3 position) { return new Vector3Int( Mathf.FloorToInt(position.x / cellSize), Mathf.FloorToInt(position.y / cellSize), Mathf.FloorToInt(position.z / cellSize) ); } public void AddBody(SimplePhysicsBody body, Vector3 position) { Vector3Int key GetCellKey(position); if (!grid.ContainsKey(key)) grid[key] new ListSimplePhysicsBody(); if (!grid[key].Contains(body)) grid[key].Add(body); } public void Clear() { grid.Clear(); } public IEnumerableSimplePhysicsBody GetPotentialCollisions(SimplePhysicsBody body, Vector3 position) { Vector3Int key GetCellKey(position); ListSimplePhysicsBody potentials new ListSimplePhysicsBody(); // 检查当前单元格及周围26个相邻单元格3x3x3区域 for (int x -1; x 1; x) { for (int y -1; y 1; y) { for (int z -1; z 1; z) { Vector3Int neighborKey key new Vector3Int(x, y, z); if (grid.TryGetValue(neighborKey, out var bodiesInCell)) { potentials.AddRange(bodiesInCell); } } } } // 移除自身 potentials.Remove(body); return potentials.Distinct(); // 去重因为一个物体可能占据多个单元格 } }在物理循环中我们先清空网格然后将所有物体根据其位置添加到网格中。之后对于每个物体只与GetPotentialCollisions返回的物体进行精确的碰撞检测。这能将复杂度从O(n²)大幅降低到接近O(n)。5.2 积分器升级从欧拉到Verlet显式欧拉法虽然简单但能量不守恒在长时间模拟或较大时间步长下容易产生误差累积导致系统“爆炸”或过度衰减。一个简单而有效的改进是Verlet积分。Verlet积分不直接存储速度而是通过前一刻的位置来计算当前速度具有更好的能量守恒特性对于保守系统。public class VerletPhysicsBody { public Vector3 currentPosition; public Vector3 previousPosition; // 上一帧的位置 public Vector3 acceleration; // 当前帧计算的加速度 public void VerletIntegrate(float deltaTime) { // Verlet 积分公式: x_new 2*x_current - x_old a * dt^2 Vector3 newPosition 2.0f * currentPosition - previousPosition acceleration * deltaTime * deltaTime; // 更新状态 previousPosition currentPosition; currentPosition newPosition; // 清除加速度为下一帧计算做准备 acceleration Vector3.zero; } // 速度可以通过位置差近似得到用于碰撞响应等 public Vector3 GetVelocity(float deltaTime) { return (currentPosition - previousPosition) / deltaTime; } public void SetVelocity(Vector3 velocity, float deltaTime) { previousPosition currentPosition - velocity * deltaTime; } }Verlet积分在实现粒子系统、布料模拟时非常流行因为它稳定且代码简洁。但需要注意的是处理速度相关的力如阻尼和碰撞响应时需要从位置差中反推速度会稍微复杂一点。5.3 约束求解刚体关节的雏形物理系统不仅仅是自由落体和碰撞还有各种约束比如铰链、滑块、弹簧。这些都可以通过约束求解来实现。其核心思想是定义物体状态必须满足的一个数学条件如“两个点之间的距离必须保持为L”然后通过迭代计算施加矫正力或直接修正位置使物体状态尽可能满足这个条件。例如一个简单的距离约束像一根硬杆连接两个粒子public static void SatisfyDistanceConstraint(Vector3 p1, Vector3 p2, float restLength, float stiffness, ref Vector3 outCorrection1, ref Vector3 outCorrection2) { Vector3 delta p2 - p1; float currentDistance delta.magnitude; if (currentDistance 0) return; Vector3 correctionDir delta.normalized; float correctionMagnitude (currentDistance - restLength) * stiffness; // 将修正量按质量倒数分配假设质量相同则均摊 outCorrection1 0.5f * correctionMagnitude * correctionDir; outCorrection2 - 0.5f * correctionMagnitude * correctionDir; }在物理循环中我们可以在位置积分后多次迭代调用所有约束的Satisfy函数逐步将物体“推”到满足约束的位置。这就是Position-Based Dynamics (PBD) 或 Sequential Impulse 等高级方法的思想基础。6. 常见问题、调试技巧与避坑指南手写物理系统时你会遇到各种光怪陆离的现象。以下是一些典型问题及解决思路。6.1 物体抖动或穿透Jitter / Tunneling原因1时间步长过大。FixedUpdate的Time.fixedDeltaTime默认是0.02s50Hz。如果物体速度非常快一帧内移动的距离可能超过其自身尺寸导致从碰撞体一侧直接“穿越”到另一侧检测不到碰撞。解决降低Time.fixedDeltaTime如提高到0.005s200Hz或者实现连续碰撞检测CCD。CCD不是检测静态位置而是检测从上一帧到这一帧的整个运动线段或扫掠体是否与其它物体相交。原因2碰撞响应与位置修正顺序/迭代次数不足。一次碰撞响应可能无法完全分离物体下一帧它们仍然嵌在一起导致再次响应产生振荡抖动。解决对同一对碰撞体在同一物理帧内进行多次如3-5次迭代的“响应-修正”循环。或者使用更高级的求解器如前面提到的Sequential Impulse它本质上就是在迭代求解。原因3数值不稳定。当质量相差巨大如一个动态物体撞上一面“静态”墙或者恢复系数设为1完全弹性时计算中可能出现极大或极小的数值导致计算溢出或产生高频振荡。解决为静态物体设置一个极大的质量如float.MaxValue并在计算冲量时用条件判断避免除以无穷大。避免使用1.0作为恢复系数0.8-0.95是更常见、更稳定的值。6.2 能量异常越飞越快或迅速停止原因积分误差或力计算错误。显式欧拉法本身就不保能量。阻尼系数设置不当也可能导致能量被过度耗散或增加。解决使用半隐式欧拉法Symplectic Euler它在更新速度后用新速度更新位置比显式欧拉更稳定。或者切换到Verlet积分。仔细检查力的计算确保阻尼力-drag * velocity的方向与速度相反。6.3 如何调试手写物理系统当物理表现异常时系统的透明性就成了你的超能力。可视化调试在OnDrawGizmos中绘制物体的速度向量、受力方向、碰撞法线、碰撞点。private void OnDrawGizmos() { if (physicsBody null) return; // 用红色线绘制速度方向 Gizmos.color Color.red; Gizmos.DrawRay(transform.position, physicsBody.velocity); // 用绿色线绘制上一帧的主要受力方向 Gizmos.color Color.green; Gizmos.DrawRay(transform.position, lastFrameForce.normalized); }数据输出在关键函数中加入Debug.Log输出每一帧的速度、力、碰撞信息。你可以比较理论计算值和实际模拟值。简化场景从两个球开始测试排除其他干扰。固定一个球的质量为极大测试碰撞响应是否正确。单元测试思维为你的物理函数如CheckSphereSphere,ResolveSphereCollision编写一些单元测试给定特定的输入验证输出是否符合物理定律。避坑终极心得从小处着手逐步验证不要试图一开始就写一个完整的、支持所有形状的物理引擎。从单个自由落体球开始确保重力积分正确。然后加入第二个球实现碰撞检测和响应。再考虑多个球和空间划分。接着尝试加入旋转角速度。最后才考虑复杂的约束和关节。每完成一步都构建一个简单的测试场景观察行为是否符合预期。这个过程中积累的不仅仅是代码更是对物理模拟每一个环节的深刻直觉。当你再回头使用Unity的PhysX时你会以一种全新的、更自信的眼光看待它因为你已经窥见过盒子内部的风景。

相关新闻

最新新闻

日新闻

周新闻

月新闻