In order to make 2D games in Urho3D, the Urho2D sublibrary is provided. Urho2D includes 2D graphics and 2D physics.
A typical 2D game setup would consist of the following:
- Create an orthographic camera
- Create some sprites
- Use physics and constraints to interact with the scene
In order to use Urho2D we need to set camera to orthographic mode first; it can be done with following code:
To zoom in/out, use SetZoom().
Urho2D provides a handful of classes for loading/drawing the kind of sprite required by your game. You can chose from animated sprites, 2D particle emitters and static sprites.
Workflow for creating animated sprites in Urho2D relies on Spriter (c). Spriter is a crossplatform tool for creating 2D animations. It comes both as an almost fully featured free version and a more advanced 'pro' version. Free version is available at http://www.brashmonkey.com/spriter.htm. To get started, scml files from bin/Data/Urho2D folder can be loaded in Spriter. Note that although currently Spriter doesn't support spritesheets/texture atlases, Urho2D does: you just have to use the same name for your scml file and your spritesheet's xml file (see Static sprites below for details on how to generate this file). Example 33_Urho2DSpriterAnimation is a good demonstration of this feature (scml file and xml spritesheet are both named 'imp' to instruct Urho2D to use the atlas instead of the individual files). You could remove every image files in the 'imp' folder and just keep 'imp_all.png' to test it out. However, keep your individual image files as they are still required if you want to later edit your scml project in Spriter.
- AnimationSet2D: a Spriter *.scml file including one or more animations. Each Spriter animation (Animation2D) contained in an AnimationSet2D can be accessed by its index and by its name (using GetAnimation()).
- AnimatedSprite2D: component used to display a Spriter animation (Animation2D) from an AnimationSet2D. Equivalent to a 3D AnimatedModel. Animation2D animations inside the AnimationSet2D are accessed by their name (String) using SetAnimation(). Playback animation speed can be controlled using SetSpeed(). Loop mode can be controlled using SetLoopMode(). You can use the default value set in Spriter (LM_DEFAULT) or make the animation repeat (LM_FORCE_LOOPED) or clamp (LM_FORCE_CLAMPED). One interesting feature is the ability to flip/mirror animations on both axes, using SetFlip(), SetFlipX() or SetFlipY(). Once flipped, the animation remains in that state until boolean state is restored to false. It is recommended to build your sprites centered in Spriter if you want to easily flip their animations and avoid using offsets for position and collision shapes.
- Animation2D (RefCounted): a Spriter animation from an AnimationSet2D. It allows readonly access to a given scml's animation name (GetName()), length (GetLength()) and loop state (IsLooped()).
For a demonstration, check examples 33_Urho2DSpriterAnimation and 24_Urho2DSprite.
Tip for naming your files:
- if an xml file has the same name as an image file in the same repository, it is assumed that it is a texture parameter file
- if an xml file has the same name as a Spriter scml file in the same repository, it is assumed that it is a spritesheet file So to prevent conflicts, scml and image files shouldn't have the exact same name.
A 2D particle emitter is built from a *.pex file (a format used by many 2D engines). A *.pex file is loaded using ParticleEffect2D class (Resource) and rendered using ParticleEmitter2D class (Drawable component):
- ParticleEffect2D: a *.pex file defining the behavior and texture of a 2D particle (ParticleEmitter2D). For an example, see bin/Data/Urho2D/greenspiral.pex
- ParticleEmitter2D: used to display a ParticleEffect2D. Equivalent to a 3D ParticleEmitter.
For a demonstration, check example 25_Urho2DParticle.
'ParticleEditor2D' tool (https://github.com/aster2013/ParticleEditor2D) can be used to easily create pex files. And to get you started, many elaborate pex samples under friendly licenses are available on the web, mostly on Github (check ParticlePanda, Citrus Engine, Particle Designer, Flambe, Starling, CBL...)
Static sprites are built from single image files or from spritesheets/texture atlases. Single image files are loaded using Sprite2D class (Resource) and spritesheets/texture atlases are loaded using SpriteSheet2D class (Resource). Both are rendered using StaticSprite2D class (Drawable component):
- Sprite2D: an image defined with texture, texture rectangle and hot spot.
- SpriteSheet2D: a texture atlas image (that packs multiple Sprite2D images).
- StaticSprite2D: used to display a Sprite2D. Equivalent to a 3D StaticModel. Spritesheets can be created using tools like ShoeBox (http://renderhjs.net/shoebox/), darkFunction Editor (http://darkfunction.com/editor/), SpriteHelper (http://www.gamedevhelper.com/spriteHelper2Info.php), TexturePacker (https://www.codeandweb.com/texturepacker), ... These tools will generate an image file and a xml file mapping coordinates and size for each individual image. Note that Urho2D uses same xml file format as Sparrow/Starling engines.
You can assign a material to an image by creating a xml parameter file named as the image and located in the same folder. For example, to make the box sprite (bin/Data/Urho2D/Box.png) nearest filtered, create a file Box.xml next to it, with the following content:
The full list of texture parameters is documented here.
Finally, note that you can easily mix both 2D and 3D resources. 3D assets' position need to be slightly offset on the Z axis (z=1 is enough), Camera's position needs to be slightly offset (on the Z axis) from 3D assets' max girth and a Light is required.
Urho2D implements rigid body physics simulation using the Box2D library. You can refer to Box2D manual at http://box2d.org/manual.pdf for full reference. PhysicsWorld2D class implements 2D physics simulation in Urho3D and is mandatory for 2D physics components such as RigidBody2D, CollisionShape2D or Constraint2D.
RigidBody2D is the base class for 2D physics object instance.
Available rigid bodies (BodyType2D) are:
- BT_STATIC: a static body does not move under simulation and behaves as if it has infinite mass. Internally, Box2D stores zero for the mass and the inverse mass. Static bodies can be moved manually by the user. A static body has zero velocity. Static bodies do not collide with other static or kinematic bodies.
- BT_DYNAMIC: a dynamic body is fully simulated. It can be moved manually by the user, but normally it moves according to forces. A dynamic body can collide with all body types. A dynamic body always has finite, non-zero mass. If you try to set the mass of a dynamic body to zero, it will automatically acquire a mass of one kilogram.
- BT_KINEMATIC: a kinematic body moves under simulation according to its velocity. Kinematic bodies do not respond to forces. They can be moved manually by the user, but normally a kinematic body is moved by setting its velocity. A kinematic body behaves as if it has infinite mass, however, Box2D stores zero for the mass and the inverse mass. Kinematic bodies do not collide with other static or kinematic bodies.
You should establish the body type at creation, using SetBodyType(), because changing the body type later is expensive.
Rigid bodies can be moved/rotated by applying forces and impulses:
- linear force (progressive/gradual):
- linear or angular impulse (brutal/immediate):
- torque (angular force):
ApplyForce() and ApplyLinearImpulse() take two parameters: the direction of the force and where to apply it. Note that in order to improve performance, you can request the body to sleep by setting 'wake' parameter to false.
You can also directly set the linear or angular velocity of the body using SetLinearVelocity() or SetAngularVelocity(). And you can get current velocity using GetLinearVelocity() or GetAngularVelocity().
To 'manually' move or rotate a body, simply translate or rotate the node to which it belongs to.
Check Box2D manual - Chapter 4 Collision Module and Chapter 7 Fixtures for full reference.
- CollisionBox2D: defines 2D physics collision box. Box shapes have an optional position offset (SetCenter()), width and height size (SetSize()) and a rotation angle expressed in degrees (SetAngle().). Boxes are solid, so if you need a hollow box shape then create one from a CollisionChain2D shape.
- Circle shapes <=> CollisionCircle2D: defines 2D physics collision circle. Circle shapes have an optional position offset (SetCenter()) and a radius (SetRadius()). Circles are solid, you cannot make a hollow circle using the circle shape.
- Polygon shapes <=> CollisionPolygon2D: defines 2D physics collision polygon. Polygon shapes are solid convex polygons. A polygon is convex when all line segments connecting two points in the interior do not cross any edge of the polygon. A polygon must have 3 or more vertices (SetVertices()). Polygons vertices winding doesn't matter.
- Edge shapes <=> CollisionEdge2D: defines 2D physics collision edge. Edge shapes are line segments defined by 2 vertices (SetVertex1() and SetVertex2() or globaly SetVertices()). They are provided to assist in making a free-form static environment for your game. A major limitation of edge shapes is that they can collide with circles and polygons but not with themselves. The collision algorithms used by Box2D require that at least one of two colliding shapes have volume. Edge shapes have no volume, so edge-edge collision is not possible.
- Chain shapes <=> CollisionChain2D: defines 2D physics collision chain. The chain shape provides an efficient way to connect many edges together (SetVertices()) to construct your static game worlds. You can connect chains together using ghost vertices. Self-intersection of chain shapes is not supported.
Several collision shapes may exist in the same node to create compound shapes. This can be handy to approximate complex or concave shapes.
Important: collision shapes must match your textures in order to be accurate. You can use Tiled's objects to create your shapes (see Tile map objects). Or you can use tools like Physics Body Editor (https://code.google.com/p/box2d-editor/), RUBE (https://www.iforce2d.net/rube/), LevelHelper (http://www.gamedevhelper.com/levelhelper/), PhysicsEditor (https://www.codeandweb.com/physicseditor), ... to help you. Other interesting tool is BisonKick (https://bisonkick.com/app/518195d06927101d38a83b66/).
Box2D fixtures are implemented through the CollisionShape2D base class for 2D physics collision shapes. Common parameters shared by every collision shape include:
- Density: SetDensity() and GetDensity()
- Friction: SetFriction() and GetFriction()
- Restitution (bounciness): SetRestitution() and GetRestitution()
CollisionShape2D class also provides readonly access to these properties:
Box2D supports collision filtering (restricting which other objects to collide with) using categories and groups:
- Collision categories:
- Collision groups: positive and negative indices assigned using SetGroupIndex(). All collision shapes within the same group index either always collide (positive index) or never collide (negative index).
- collision group has higher precedence than collision category
- a collision shape on a static body can only collide with a dynamic body
- a collision shape on a kinematic body can only collide with a dynamic body
- collision shapes on the same body never collide with each other
A collision shape can be set to trigger mode to only report collisions without actually applying collision forces. This can be used to implement trigger areas. Note that:
- a sensor can be triggered only by dynamic bodies (BT_DYNAMIC)
- physics queries don't report triggers. To get notified when a sensor is triggered or cease to be triggered, subscribe to E_PHYSICSBEGINCONTACT2D and E_PHYSICSENDCONTACT2D physics events.
Constraints ('joints' in Box2D terminology) are used to constrain bodies to an anchor point or between themselves. Apply a constraint to a node (called 'ownerBody') and use SetOtherBody() to set the other node's body to be constrained to the ownerBody.
See 32_Urho2DConstraints sample for detailed examples and to help selecting the appropriate constraint. Following are the available constraints classes, with the indication of the corresponding 'joint' in Box2D manual (see Chapter 8 Joints):
- Constraint2D: base class for 2D physics constraints.
- Distance joint <=> ConstraintDistance2D: defines 2D physics distance constraint. The distance between two anchor points (SetOwnerBodyAnchor() and SetOtherBodyAnchor()) on two bodies is kept constant. The constraint can also be made soft, like a spring-damper connection. Softness is achieved by tuning frequency (SetFrequencyHz() is below half of the timestep) and damping ratio (SetDampingRatio()).
- Revolute joint <=> ConstraintRevolute2D: defines 2D physics revolute constraint. This constraint forces two bodies to share a common hinge anchor point (SetAnchor()). You can control the relative rotation of the two bodies (the constraint angle) using a limit and/or a motor. A limit (SetEnableLimit()) forces the joint angle to remain between a lower (SetLowerAngle()) and upper (SetUpperAngle()) bound. The limit will apply as much torque as needed to make this happen. The limit range should include zero, otherwise the constraint will lurch when the simulation begins. A motor (SetEnableMotor()) allows you to specify the constraint speed (the time derivative of the angle). The speed (SetMotorSpeed()) can be negative or positive. When the maximum torque (SetMaxMotorTorque()) is exceeded, the joint will slow down and can even reverse. You can use a motor to simulate friction. Just set the joint speed to zero, and set the maximum torque to some small, but significant value. The motor will try to prevent the constraint from rotating, but will yield to a significant load.
- Prismatic joint <=> ConstraintPrismatic2D: defines 2D physics prismatic constraint. This constraint allows for relative translation of two bodies along a specified axis (SetAxis()). There's no rotation applied. This constraint definition is similar to ConstraintRevolute2D description; just substitute translation for angle and force for torque.
- Pulley joint <=> ConstraintPulley2D: defines 2D physics pulley constraint. The pulley connects two bodies to ground (SetOwnerBodyGroundAnchor() and SetOtherBodyGroundAnchor()) and to each other (SetOwnerBodyAnchor() and SetOtherBodyAnchor()). As one body goes up, the other goes down. You can supply a ratio (SetRatio()) that simulates a block and tackle. This causes one side of the pulley to extend faster than the other. At the same time the constraint force is smaller on one side than the other. You can use this to create mechanical leverage.
- Gear joint <=> ConstraintGear2D: defines 2D physics gear constraint. Used to create sophisticated mechanisms and saves from using compound shapes. This constraint can only connect ConstraintRevolute2Ds and/or ConstraintPrismatic2Ds (SetOwnerConstraint() and SetOtherConstraint()). Like the pulley ratio, you can specify a gear ratio (SetRatio()). However, in this case the gear ratio can be negative.
- Mouse joint <=> ConstraintMouse2D: defines 2D physics mouse constraint. Used to manipulate bodies with the mouse, this constraint is almost used in every Box2D tutorial available on the net, to allow interacting with the 2D scene. It attempts to drive a point on a body towards the current position of the cursor. There is no restriction on rotation. This constraint has a target point, maximum force, frequency, and damping ratio. The target point (SetTarget()) initially coincides with the body’s anchor point. The maximum force (SetMaxForce()) is used to prevent violent reactions when multiple dynamic bodies interact. You can make this as large as you like. The frequency (SetFrequencyHz()) and damping ratio (SetDampingRatio()) are used to create a spring/damper effect similar to the ConstraintDistance2D. Many users have tried to adapt the ConstraintMouse2D for game play. Users often want to achieve precise positioning and instantaneous response. The ConstraintMouse2D doesn’t work very well in that context. You may wish to consider using kinematic bodies instead.
- Wheel joint <=> ConstraintWheel2D: defines 2D physics wheel constraint. This constraint restricts a point on bodyB (SetAnchor()) to a line on bodyA (SetAxis()). It also provides a suspension spring.
- Weld joint <=> ConstraintWeld2D: defines 2D physics weld constraint. This constraint attempts to constrain all relative motion between two bodies.
- Rope joint <=> ConstraintRope2D: defines 2D physics rope constraint. This constraint restricts the maximum distance (SetMaxLength()) between two points (SetOwnerBodyAnchor() and SetOtherBodyAnchor()). This can be useful to prevent chains of bodies from stretching, even under high load.
- Friction joint <=> ConstraintFriction2D: defines 2D physics friction constraint. This constraint is used for top-down friction. It provides 2D translational friction (SetMaxForce()) and angular friction (SetMaxTorque()).
- Motor joint <=> ConstraintMotor2D: defines 2D physics motor constraint. This constraint lets you control the motion of a body by specifying target position (SetLinearOffset()) and rotation offsets (SetAngularOffset()). You can set the maximum motor force (SetMaxForce()) and torque (SetMaxTorque()) that will be applied to reach the target position and rotation. If the body is blocked, it will stop and the contact forces will be proportional to the maximum motor force and torque.
Collision between bodies connected by a constraint can be enabled/disabled using SetCollideConnected().
The following queries into the physics world are provided:
- Shape point test: test if a point is inside a given plain shape and returns the body if true. Use GetRigidBody(). Point can be a Vector2 world position, or more conveniently you can pass screen coordinates when performing the test from an input (mouse, joystick, touch). Note that only plain shapes are supported, this test is not applicable to CollisionChain2D and CollisionEdge2D shapes.
- Shape ray cast: returns the body, distance, point of intersection (position) and normal vector for the first shape hit by the ray. Use RaycastSingle().
- Overlap between 2 shapes: not implemented
- Contact manifolds (contact points for overlaping shapes): not implemented
- Distance between 2 shapes: not implemented
- Time of impact (time when 2 moving shapes collide): not implemented
- AABB queries: return the bodies overlaping with the given rectangle. See GetRigidBodies().
- Ray casts: return the body, distance, point of intersection (position) and normal vector for every shape hit by the ray. See Raycast().
Contact listener (see Box2D manual, Chapter 9 Contacts) enables a given node to report contacts through events. Available events are:
- E_PHYSICSBEGINCONTACT2D ("PhysicsBeginContact2D" in script): called when 2 collision shapes begin to overlap
- E_PHYSICSENDCONTACT2D ("PhysicsEndContact2D" in script): called when 2 collision shapes cease to overlap
- E_PHYSICSPRESTEP2D ("PhysicsPreStep2D" in script): called after collision detection, but before collision resolution. This allows to disable the contact if need be (for example on a one-sided platform). Currently ineffective (only reports PhysicsWorld2D and time step)
- E_PHYSICSPOSTSTEP2D ("PhysicsPostStep2D" in script): used to gather collision impulse results. Currentlly ineffective (only reports PhysicsWorld2D and time step)
Tile maps workflow relies on the tmx file format, which is the native format of Tiled, a free app available at http://www.mapeditor.org/. It is strongly recommended to use stable release 0.9.1. Do not use daily builds or other newer/older stable revisions, otherwise results may be unpredictable.
Check example 36_Urho2DTileMap for a basic demonstration.
You can use tile maps for the design of the whole scene/level, or in adjunction to other 2D resources.
A tmx file is loaded using TmxFile2D resource class and rendered using TileMap2D component class. You just have to create a TileMap2D component inside a node and then assign the tmx resource file to it.
- currently only XML Layer Format is supported (Base64 and CSV are not). In Tiled, go to Maps > Properties to set 'Layer Format' to 'XML'.
- if 'seams' between tiles are obvious then you should make your tilesets images nearest filtered (see Static sprites section above.)
A map is defined by its:
- orientation: Urho2D supports both orthogonal (flat) and isometric (strict iso 2.5D and staggered iso) tile maps. Orientation can be retrieved with orientation_ attribute (O_ORTHOGONAL for ortho, O_ISOMETRIC for iso and O_STAGGERED for staggered)
- width and height expressed as a number of tiles in the map: use width_ and height_ attributes to access these values
- width and height expressed in Urho2D space: use GetMapWidth() and GetMapHeight() to access these values which are useful to set the camera's position for example
- tile width and tile height as the size in pixels of the tiles in the map (equates to Tiled width/height * PIXEL_SIZE): use tileWidth_ and tileHeight_ attributes to access these values
Two convenient functions are provided to convert Tiled index to/from Urho2D space:
- TileIndexToPosition() to convert tile index to Urho position
- PositionToTileIndex() to convert Urho position to tile index (returns false if position is outside of the map)
You can display debug geometry for the whole tile map using DrawDebugGeometry().
A tile map is built from fixed-size sprites ('tiles', accessible from the Tile2D class) belonging to one or more 'tilesets' (=spritesheets). Each tile is characterized by its:
- grid ID (ID in the tileset, from top-left to bottom-right): use GetGid()
- sprite/image (Sprite2D): use GetSprite()
- property: use HasProperty() and GetProperty()
Tiles from a tileset can only be accessed from one of the layers they are 'stamped' onto, using GetTile() (see next section).
A layer is characterized by its:
- name: currently not accessible
- width and height expressed as a number of tiles: use GetWidth() and GetHeight() to access these values
- type: retrieved using GetLayerType() (returns the type of layer, a TileMapLayerType2D: Tile=LT_TILE_LAYER, Object=LT_OBJECT_GROUP, Image=LT_IMAGE_LAYER and Invalid=LT_INVALID)
- custom properties : use HasProperty() and GetProperty() to check/access these values
Layer visibility can be toggled using SetVisible() (and visibility state can be accessed with IsVisible()). Currently layer opacity is not implemented. Use DrawDebugGeometry() to display debug geometry for a given layer.
By default, first tile map layer is drawn on scene layer 0 and subsequent layers are drawn in a 10 scene layers step. For example, if your tile map has 3 layers:
- bottom layer is drawn on layer 0
- middle layer is on layer 10
- top layer is on layer 20
- to access a tile node, which enables access to the StaticSprite2D component, for example to remove it or replace it, use GetTileNode()
- to access a tileset's Tile2D tile, which enables access to the Sprite2D resource, gid and custom properties (as mentioned above), use GetTile()
Tiled objects are wire shapes (Rectangle, Ellipse, Polygon, Polyline) and sprites (Tile) that are freely positionable in the tile map.
Accessing Tiled objects : from a TileMapLayer2D layer, objects are accessed by their index using GetObject(). GetNumObjects() returns the number of objects contained in the object layer (tile and image layers will return 0 as they don't hold objects).
Use GetObjectType() to get the nature of the selected object (TileMapObjectType2D: OT_RECTANGLE for Rectangle, OT_ELLIPSE for Ellipse, OT_POLYGON for Polygon, OT_POLYLINE for PolyLine, OT_TILE for Tile and OT_INVALID if not a valid object).
Except Tile, objects are not visible (although you can display them for debugging purpose using DrawDebugGeometry() at the level of the tile map or a given layer, as mentioned previously). They can be used:
- to easily design polygon sprites and Box2D shapes using the object's vertices: use GetNumPoints() to get the number of vertices and GetPoint() to iterate through the vertices
- as placeholders to easily set the position and size of entities in the world, using GetPosition() and GetSize()
- to display Tile objects as sprites
- to create a background from Tile sprites
If need be you can access the grid id (relative to the tilesets used) of a Tile object using GetTileGid().