Since then, it happened to encounter new challenges, which forced to expand and modify the system.
All the advantages and disadvantages, as well as a small experience I will consider the example of a very simple 2D physics with gravity, forces and collisions. Welcome to a cat, my friends!
introduction
In his free time intensively engaged in his own project, which was created exclusively for the sake of learning, but because just teeming with bicycles. Probably the "engine" - one.
Note: of course, I understand that in the context of real problems it is advisable to take a ready-made solution and use it as the woodcutter takes the ax and chop wood to them. Just sometimes you want to understand how these axes, hell, are arranged.
Current state
Since the last story was not much time, but for a few nights I realized that there are a number of tasks that the system has not yet been able to solve.
So let's briefly go through the fact that already know how to:
create independent components;
store components in containers and have access to them;
use interaction layer, providing "communication" between the containers;
impose preconditions on the presence of certain components in the containers involved in the methods of connection (Attach) and interaction (Interaction).
New issues
Long I thought what to do: first, to tell the task, during which there were problems, or to show the problems of their way to solve, and then the task itself. Eventually, he stopped for a second embodiment. First, we learn about the innovations and extensions, we consider them in detail, and then, armed with fresh data, solve the task.
And yet, what the gears are not enough to work at full capacity?
First, in the previous section I did not mention that the plan to introduce the mechanism of messages with which one component can send (within the parent container) message to another. As readers may have noticed, event approach will win flexibility without impact on architecture. Especially because no one imposes: "I am sending a message to all the components, and you want - listen, do not want - do not have to."
Secondly, there was uncertainty with preconditions. What if sometimes, in case of non compliance with the precondition, you must completely break down the implementation of programs under the pretext of breach of contract (rendering texture component does not make sense without the texture component), but you can ignore the other cases? "Now you do not have this component, but once it appears!".
Apart from that there were minor flaws that are likely to remain as it is.
Preconditions
Very quickly tell you about preconditions, because the solution is quite simple. New listing:
public enum OnValidationFail
{
Throw,
Skip
}
And yet! Now it can be used as follows:
[AttachHandler (OnValidationFail.Skip)]
Forwarding messages
With shipment interesting posts. Firstly, each component can now position itself as the sender and the recipient.
Send possible messages that are described very simply in general terms:
public class Message <T>: Message
{
public T Sender {get; private set; }
public Message (T sender)
{
Sender = sender;
}
}
For I have used the same approach again (for the third time) using reflection, which makes me very intrusive ease and flexibility.
Every component can be described in the body of the class methods that take as a parameter to any Message. Make it really helps the recipient attribute [MessageReceiver]. For example:
public class SimpleComponent: Component
{
[MessageReceiver]
public void MessageFromFriend (DetachMessage <FriendComponent> message)
{
// Hey, you want a friendly component to remove from the container.
// Who are you without it?
_container.RemoveComponent (this);
}
[MessageReceiver]
public void MessageFromSomeComponent (DetachMessage <SomeComponent> message)
{
// Neutral component does not concern you, that it is removed.
// Once it touched, but no longer.
//_container.RemoveComponent(this);
}
}
Because obviously the presence of such a component life cycle stages as adding and deleting, shustrenko write protected methods utility in the Component class, which allow the heirs if you want very briefly to inform all the "neighbors" of the current status.
protected void SendAttachMessage <TSender, TContainer>
(TSender sender, TContainer container)
where TSender: Component
{
SendMessage (
new ComponentAttachMessage <TSender, TContainer> (sender, container));
}
protected void SendDetachMessage <T> (T sender) where T: Component
{
SendMessage (new ComponentDetachMessage <T> (sender));
}
On this with the main "major" modifications (just one, in fact) everything. Now I tell about the immediate area of application not only new features, but also, in general, the system.
The problem with gravity
At the initial stage of development arose following a trivial task: to make at least so that the scene could put two kinds of "boxes":
those that are falling, but are able to land on the other;
those that do not know how to fall; and land on them first.
It sounds very simple, but it is not clear whether the strength svezhesproektirovannoy system?
Total solutions sketch unwrap in a logical sequence, without reference to the implementation of:
1. Let there game objects that can be placed on the stage of the coordinates X, Y and render.
2. Introduce the concept of a rigid body - the properties of the object of the game, which allows you to ask him to change the speed of the corresponding coordinates.
3. In principle, the motion of an object in the game has it all: location, a mechanism that makes it possible to change them. Not enough thrust, in other words, force.
4. What else? To be able to move objects on the stage - that's fine, but we still need, after all, to teach them to face. To do this, we define the concept of a shell object, responsible for collision
Game objects
Following the above sketch, select primal essence - a game object that will, in combination, the components of the container.
public class GameObject: ComponentContainer
{
}
Not every game object can be placed on the stage, so we introduce a successor - the object scene.
public class SceneObject: GameObject
{
public float X {get; set; }
public float Y {get; set; }
public event Action <TimeSpan> Updating;
public event Action <TimeSpan> Drawing;
public void Draw (TimeSpan deltaTime)
{
Updating (deltaTime);
}
public void Updated (TimeSpan deltaTime)
{
Drawing (deltaTime);
}
}
Note: not to write a lot of unnecessary code, some of the implementation details I will omit, as the most important thing - it is an idea, but that is not checked and Updated Drawn for null or not signed pacifiers - a side issue.
solids
Since solids - a game object properties (they may or may not be), they can be encapsulated in the components. Let's see the code:
public class RigidBody: Component
{
private SceneObject _sceneObject;
private float _newX;
private float _newY;
public float VelocityX {get; set; }
public float VelocityY {get; set; }
[AttachHandler]
public void OnSceneObjectAttach (SceneObject sceneObject)
{
_sceneObject = sceneObject;
_sceneObject.Updating + = OnUpdate;
_sceneObject.Drawing + = OnDraw;
}
private void OnUpdate (TimeSpan deltaTime)
{
// Here we will calculate the new coordinates of the object based
// The speed.
if (VelocityX! = 0.0f)
_newX = (float) (_sceneObject.X +
(VelocityX * deltaTime.TotalSeconds));
if (VelocityY! = 0.0f)
_newY = (float) (_sceneObject.Y +
(VelocityY * deltaTime.TotalSeconds));
}
private void OnDraw (TimeSpan deltaTime)
{
// At the stage of drawing moving objects.
_sceneObject.X = _newX;
_sceneObject.Y = _newY;
}
}
I hope I'm stupid enough to come up with some really significant and complex physical structure that everyone understands what's inside the previous piece of code.
of force
So we got to the last key to "eternal" movement. Strength - this is, in fact, just something that changes the speed of solids. It remains to understand what and how to change the speed.
Note: The most likely someone has already solved a thousand times this task is the same, but, remember, the training project - very much like to reach any decisions. See more about php storm released.
What has come up?
Let's look at the so-called "unit circle", where the values in the axis oX and oY submitted to the range of [-1, 1].
For example, a blue arrow - is the vector of the force on the body, and the center of the circle - the center of gravity of the body. And, say notional value of the most power - 100.
If the force changes the speed of the object, it must change its speed both vertically and horizontally, as well as - we will answer the green and orange lines. They are approximately 0.9 and 0.5 according to, respectively.
Therefore, the speed change in the vertical object 100 * 0.9, and horizontal - 100 * 0.5.
Fixed the code very simple reasoning:
public class Force
{
public int Angle {get; set; }
public float Power {get; set; }
public void Add (RigidBody rigidBody)
{
// Translate degrees to radians (as necessary).
var radians = GeometryUtil.DegreesToRadians (Angle);
var horizontalCoefficient = GetHorizontalCoefficient (radians);
var verticalCoefficient = GetVerticalCoefficient (radians);
rigidBody.VelocityX + = Power * horizontalCoefficient;
rigidBody.VelocityY + = Power * verticalCoefficient;
}
private float GetHorizontalCoefficient (double radians)
{
var scaleX = Math.Cos (radians);
if (Math.Abs (scaleX) <= 0) return 0;
return (float) scaleX;
}
private float GetVerticalCoefficient (double radians)
{
// Here we multiply by -1, as games oY coordinate system axis is inverted.
var scaleY = Math.Sin (radians) * -1;
if (Math.Abs (scaleY) <= 0) return 0;
return (float) scaleY;
}
}
I want to note that the class of Force and not the component, and not the container. This is a primitive type, which subsequently will re-use by others.
It is felt that it is not enough for someone who could impose a force on hard body, right?
gravitation
What caused the most difficulties. We wanted to make something like a gravitational field, but it was not clear where it is taken and how to apply to the objects. The component is or container?
In the end, gathered his thoughts, he decided that component. The component that is added to any objects present in a gravitational field.
The objective component - each update cycle the power to impose an angle of 270 degrees (bottom) with the value of 9.83.
[RequiredComponent (typeof (RigidBody))]
public class Gravitation: Component
{
private SceneObject _sceneObject;
private RigidBody _rigidBody;
private Force _gravitationForce = new Force (270, 9.83);
[AttachHandler (OnValidationFail.Skip)]
public void OnSceneObjectAttach (SceneObject sceneObject)
{
_sceneObject = sceneObject;
_rigidBody = GetComponent <RigidBody> ();
_sceneObject.Updating + = OnUpdate;
}
private void OnUpdate (TimeSpan deltaTime)
{
_gravitationForce.Add (_rigidBody);
}
// In case you have gravity, and solid state - no.
[MessageReceiver]
public void OnRigidBodyAttach (
ComponentAttachMessage <RigidBody, SceneObject> message)
{
_rigidBody = message.Sender;
_sceneObject = message.Container;
_sceneObject.Updating + = OnUpdate;
}
[MessageReceiver]
public void OnRigidBodyDetach (ComponentDetachMessage <RigidBody> message)
{
_sceneObject.Updating - = OnUpdate;
}
}
So now, even if the object is not solid-state component, gravity will not break. It just will not work as long as the above-mentioned components will not add to the container.
Example turned demonstrative, clearly illustrating the situation where one component sends a message to another within the parent container.
Clashes
There was the most interesting part of any physics engine, which is based on the universe.
From sketch solving the problem that the shell is responsible for the collision - is also a property of the object, as well as a solid body. So do not hesitate to define it as a component.
Next, I figured that the shell are different: the most simple - a rectangular, but there are round, which are also easy to take it, and finally an arbitrary shape shell with N vertices.
It is necessary at some point to calculate a collision between two different membranes. For example, the range fell to a rectangular ground.
We describe an abstract class shell:
public abstract class Collider: Component
{
protected SceneObject SceneObject;
[AttachHandler]
public OnSceneObjectAttach (SceneObject sceneObject)
{
SceneObject = sceneObject;
}
// Set methods for collision.
public abstract bool ResolveCollision (Collider collider);
public abstract bool ResolveCollision (BoxCollider collider);
}
The basic idea is that in order to render a collision ResolveCollision method will be used, which takes a parameter of the base type Collider, but in this method, each individual version of the shell will forward the call to another method, able to work with a particular type. Bal runs ad-hoc polymorphism.
I'll show a simple example of a rectangle:
public class BoxCollider: Collider
{
public float Width {get; set; }
public float Height {get; set; }
public RectangleF GetBounds ()
{
return new RectangleF (SceneObject.X, SceneObject.Y, Width, Height);
}
public override bool ResolveCollision (Collider collider)
{
return collider.ResolveCollision (this);
}
public override bool ResolveCollision (BoxCollider boxCollider)
{
var bounds = GetBounds ();
var colliderBounds = boxCollider.GetBounds ();
if return false (bounds.IntersectsWith (colliderBounds)!);
// Note: we actually change the value in the variable bounds.
// Since the subject moves too quickly, it can not simply "faced"
// To another, but also the last to cross the border to some value.
// This code snippet we move it back (so far only vertical).
bounds.Intersect (colliderBounds);
if (bounds.Height <= 1f) return false;
SceneObject.Y - = bounds.Height;
return true;
}
}
With the main problems sorted out: you can now create game objects, give them different properties, move, push. Not enough last little detail: where to calculate the collision?
Scene
If there SceneObject, it means to be the stage on which they are located. It is responsible for updating the states of objects and their rendering. She will assume the collision.
This helps the same layer Interaction, which still have not found application in solving the problem of gravity. We describe a class Interactor, which is responsible for the miscalculation of a collision between two objects in the scene:
public class CollisionDetector: Interactor
{
[InteractionMethod]
[RequiredComponent ( "first", typeof (Collider))]
[RequiredComponent ( "second", typeof (Collider))]
public void SceneObjectsInteraction (SceneObject first, SceneObject second)
{
var firstCollider = first.GetComponent <Collider> ();
var secondCollider = second.GetComponent <Collider> ();
if return (firstCollider.ResolveCollision (secondCollider)!);
// Here you can see that it is sometimes necessary to act, depending on the presence of a component in a container, anyway.
first.IfContains <RigidBody> (TryAddInvertedForce);
second.IfContains <RigidBody> (TryAddInvertedForce);
}
private void TryAddInvertedForce (RigidBody rigidBody)
{
var lastAddedForce = rigidBody.ForceHistory.Last ();
var invertedForce = new Force
{
Angle = GeometryUtil.GetOppositeAngle (lastAddedForce.Angle),
Power = rigidBody.GetKineticEnergyY ()
};
invertedForce.Add (rigidBody);
}
}
Note: this example is not indicative of the application of knowledge from the field of physics. It does not work for the two colliding bodies in the air that moved at a certain speed, but clearly shows an idea of how this looks for a simple fall.
The general concept is this: when the object falls to the ground and facing it, the land compensates for the speed of the fall of the new force, which acts in the opposite direction of fall and equal to the current object's kinetic energy.
Interactor himself called from the stage in the method of renovation, which is performed every frame.
In the mechanism of collision is calculated, yet use a rough O (n2) method.
// Code of the scene class.
public void Update (TimeSpan deltaTime)
{
foreach (var sceneObject in _sceneObjects)
{
sceneObject.Update (deltaTime);
foreach (var anotherSceneObject in _sceneObjects)
{
if (ReferenceEquals (sceneObject, anotherSceneObject)) continue;
sceneObject.Interact (anotherSceneObject) .Using <CollisionDetector> ();
}
}
}
conclusion
Honestly, a small structure physics engine, in fact, seemed at first not so obvious. Especially collisions and gravity. I'm pretty sure that it was unlikely the physical engines are written this way, but it was fun to try and "feel out" for himself.
In the process to allocate some of the obvious advantages of component-oriented approach, which is based both on my observations and common sense:
1. Write the code is very simple, when you clearly see what makes the component and on whom he depends (it is evident).
2. The code is very flexible and independent. Each component has observed unambiguous and the degree of influence on the container.
It is highly probable that the problem was relatively simple, and at higher loads, the system will fail and begin to contribute to the excessive complexity of the code. I think it'll check in soon.
No comments:
Post a Comment