4
\$\begingroup\$

I am writing a game like Pong. Therefore I am using an entity system approach, so I use a Component-based design.

The hardest part until now was to write the collision system. As I just began with the component based design I am sometimes very unsure how to do something with this approach. With a "normal" OOP approach I would just have a ball class which has special collision, and a paddle class which interacts with the ball etc.

But here it is different as I am not able to tell which object I am looking at because I just have a collection of components. So I can only know what the object could do and what data it holds.

My code is in Blitzmax, which is not used that often.

Here is the code of the system:

Rem
bbdoc: Collision system to handly collisions between entities.
End Rem
Type TCollision_System Extends TSystem
rem
    bbdoc: If objects collide, one of them is moved back by this distance.
endrem
Const MOVE_AWAY_DISTANCE:Int = 2

rem
    bbdoc: Constructor for collision system. Here are just all initial things done.
endrem
Function Create:TSystem(entity_Manager:TEntityManager)
    Local system:TCollision_System = New TCollision_System

    system.entity_Manager = entity_Manager
    system.required_Components = New TList
    system._init_Required_Components()

    Return system
End Function

Rem
    bbdoc: Required component types are registered in the list to get compatible entities.
endrem
Method _init_Required_Components()
    Self.required_Components.AddLast(TComponent_Type.COLLISION_COMPONENT)
    Self.required_Components.AddLast(TComponent_Type.POSITION_COMPONENT)
    Self.required_Components.AddLast(TComponent_Type.RENDER_COMPONENT)
End Method

Rem
    bbdoc: Here the actual collision is done. Every possible kind of collision is handled here.
endrem
Method update()
    'Get all compatible entities to check them for collision.
    Local entities_Collision:TList = Self.entity_Manager.get_All_Entities_With_Components_With_Type(Self.required_Components)

    For Local entity_One:TEntity = EachIn entities_Collision
        Local render_Components_One:TList = Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.RENDER_COMPONENT, entity_One)
        Local position_Components_One:TList = Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.POSITION_COMPONENT, entity_One)
        Local collision_Components_One:TList = Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.COLLISION_COMPONENT, entity_One)

        'We assume that only one component is used for collision, so we do not iterate through the lists of components.
        Local render_One:TRender_Component = TRender_Component(render_Components_One.First())
        Local position_One:TPosition_Component = TPosition_Component(position_Components_One.First())
        Local collision_One:TCollision_Component = TCollision_Component(collision_Components_One.First())

         For Local entity_Two:TEntity = EachIn entities_Collision
            'Do nothing if the both objects are the same - An object cannot collide with itself.
            If(entity_One = entity_Two) Then
                Continue
            EndIf

            Local render_Components_Two:TList = Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.RENDER_COMPONENT, entity_Two)
            Local position_Components_Two:TList = Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.POSITION_COMPONENT, entity_Two)
            Local collision_Components_Two:TList = Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.COLLISION_COMPONENT, entity_Two)

            'As above, only one component is needed, not the complete list.
            Local render_Two:TRender_Component = TRender_Component(render_Components_Two.First())
            Local position_Two:TPosition_Component = TPosition_Component(position_Components_Two.First())
            Local collision_Two:TCollision_Component = TCollision_Component(collision_Components_Two.First())

            'If both objects are static, then do nothing, because actually there is no collision, as they never moved.
            If(collision_One.get_Is_Static() And collision_Two.get_Is_Static()) Then
                Continue
            EndIf

            If(ImagesCollide(render_One.get_Image(), position_One.get_X(), position_One.get_Y(), 0, render_Two.get_Image(), position_Two.get_X(), position_Two.get_Y(), 0)) Then
                'As long as the images collide, we have to move the objects.
                While(ImagesCollide(render_One.get_Image(), position_One.get_X(), position_One.get_Y(), 0, render_Two.get_Image(), position_Two.get_X(), position_Two.get_Y(), 0))

                    'Calculate the outer positions of the first object (this is just where the image ends).
                    Local max_Point_One:TVector2 = TVector2.Create(position_One.get_X() + render_One.get_Frame_Width() / 2.0, position_One.get_Y() + render_One.get_Frame_Height() / 2.0)
                    Local min_Point_One:TVector2 = TVector2.Create(position_One.get_X() - render_One.get_Frame_Width() / 2.0, position_One.get_Y() - render_One.get_Frame_Height() / 2.0)

                    'Calculate the outer positions of the second object (where the iamge ends).
                    Local max_Point_Two:TVector2 = TVector2.Create(position_Two.get_X() + render_Two.get_Frame_Width() / 2.0, position_Two.get_Y() + render_Two.get_Frame_Height() / 2.0)
                    Local min_Point_Two:TVector2 = TVector2.Create(position_Two.get_X() - render_Two.get_Frame_Width() / 2.0, position_Two.get_Y() - render_Two.get_Frame_Height() / 2.0)

                    'First: Check if the objects overlap on the y-axis.
                    If((position_One.get_Y() <= max_Point_Two.get_Y() And position_One.get_Y() >= min_Point_Two.get_Y()) Or (position_Two.get_Y() <= max_Point_One.get_Y() And position_Two.get_Y() >= min_Point_One.get_Y())) Then
                        'They also have to overlap on the x-axis.
                        If(max_Point_Two.get_X() > min_Point_One.get_X() And min_Point_Two.get_X() < max_Point_One.get_X()) Then
                            'Decide which object has to be pushed in which direction.
                            If(min_Point_Two.get_X() < min_Point_One.get_X()) Then
                                If(Not(collision_Two.get_Is_Static_X())) Then
                                    position_Two.add_X(-MOVE_AWAY_DISTANCE)
                                Else
                                    If(Not(collision_One.get_Is_Static_X())) Then
                                        position_One.add_X(MOVE_AWAY_DISTANCE)
                                    EndIf
                                EndIf
                            Else
                                If(Not(collision_Two.get_Is_Static_X())) Then
                                    position_Two.add_X(MOVE_AWAY_DISTANCE)
                                Else
                                    If(Not(collision_One.get_Is_Static_X())) Then
                                        position_One.add_X(-MOVE_AWAY_DISTANCE)
                                    EndIf
                                EndIf
                            EndIf
                        EndIf
                    EndIf

                    'First: Check if the objects overlap on the x-axis.
                    If((position_One.get_X() <= max_Point_Two.get_X() And position_One.get_X() >= max_Point_Two.get_X()) Or (position_Two.get_X() <= max_Point_One.get_X() And position_Two.get_X() >= min_Point_One.get_X())) Then
                        'They also have to overlap on the y-axis.
                        If(max_Point_Two.get_Y() > min_Point_One.get_Y() And min_Point_Two.get_Y() < max_Point_One.get_Y()) Then
                            'Decide which object has to be pushed in which direction.
                            If(min_Point_Two.get_Y() < min_Point_One.get_Y()) Then
                                If(Not(collision_Two.get_Is_Static_Y())) Then
                                    position_Two.add_Y(-MOVE_AWAY_DISTANCE)
                                Else
                                    If(Not(collision_One.get_Is_Static_Y())) Then
                                        position_One.add_Y(MOVE_AWAY_DISTANCE)
                                    EndIf
                                EndIf
                            Else
                                If(Not(collision_Two.get_Is_Static_Y())) Then
                                    position_Two.add_Y(MOVE_AWAY_DISTANCE)
                                Else
                                    If(Not(collision_One.get_Is_Static_Y())) Then
                                        position_One.add_Y(MOVE_AWAY_DISTANCE)
                                    EndIf
                                EndIf
                            EndIf
                        EndIf
                    EndIf

                Wend

                'Now handle the collision of each object alone. This is used for physic things.
                If(Not(collision_One.get_Is_Static())) Then
                    Self._handle_Collision_Single(entity_One, position_One, render_One, position_Two, render_Two)
                EndIf
                If(Not(collision_Two.get_Is_Static())) Then
                    Self._handle_Collision_Single(entity_Two, position_Two, render_Two, position_One, render_One)
                EndIf
            EndIf
         Next
    Next
End Method

Rem
    bbdoc: Here everything which should be only done once per collision is done with a single object.
endrem
Method _handle_Collision_Single(entity_Active:TEntity, position_Component:TPosition_Component, render_Component:TRender_Component, position_Passive:TPosition_Component, render_Passive:TRender_Component)
    'Actually it should not happen. But better check for NULL reference.
    If(Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.movement_Component, entity_Active) <> Null) Then
        Local movement_Components_List:TList = Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.MOVEMENT_COMPONENT, entity_Active)

        'Here as well: We use only one movement component.
        Local movement_Component:TMovement_Component = TMovement_Component(movement_Components_List.First())

        'If the entity has a physics component, then calculate everything. Otherwise just set velocity to zero.
        If(Self.entity_Manager.get_Components_With_Type_Of_Entity(TComponent_Type.physics_Component, entity_Active) <> Null) Then
            'Get the borders of the second object (only x-axis)
            Local x_Pos_Max_Two:Float = position_Passive.get_X() + render_Passive.get_Frame_Width() / 2.0
            Local x_Pos_Min_Two:Float = position_Passive.get_X() - render_Passive.get_Frame_Width() / 2.0

            'Get the borders of the actual object (only x-axis)
            Local x_Pos_Max_One:Float = position_Component.get_X() + render_Component.get_Frame_Width() / 2.0
            Local x_Pos_Min_One:Float = position_Component.get_X() - render_Component.get_Frame_Width() / 2.0

            Local is_normal_Horizontally:Byte

            'Check if the objects collide on the x-axis or the y-axis.
            If(x_Pos_Min_One <= x_Pos_Max_Two And x_Pos_Max_One >= x_Pos_Min_Two) Then
                is_normal_Horizontally = True
            Else
                is_normal_Horizontally = False
            EndIf

            'Get the normal vector of the second object (the passive object) to calculate reflection.
            Local normal_Vector:TVector2 = Self.get_Normal(position_Passive, render_Passive, is_normal_Horizontally)

            'Store the velocity as a vector to handle it more easily.
            Local velocity:TVector2 = TVector2.Create(movement_Component.get_X_Velocity(), movement_Component.get_Y_Velocity())

            'Here the reflected vector is calculated.
            'Let r be the reflected vector. 
            'Then it is: r = v - 2 * n * (n*v), where n is the normal vector of the passive object and v is the velocity vector.
            Local scalar:Float = normal_Vector.scalar_Multiplication(velocity) * 2
            Local second_Part:TVector2 = normal_Vector.scale_New(scalar)
            second_Part.scale(-1.0)
            Local new_Velocity:TVector2 = velocity.add_New(second_Part)

            Rem
                   \v  |n  /r
                    \  |  /
                     \ | /
                 _____\|/_____
            endrem

            'Set the new velocity vector.
            movement_Component.set_X_Velocity(new_Velocity.get_X())
            movement_Component.set_Y_Velocity(new_Velocity.get_Y())
        Else
            movement_Component.set_X_Velocity(0)
            movement_Component.set_Y_Velocity(0)
        EndIf
    EndIf
End Method

Rem
    bbdoc: Calculates the normal vector of a line which is given by the passive object.
endrem
Method get_Normal:TVector2(position:TPosition_Component, render:TRender_Component, horizontally:Byte)
    Local start_Point:TVector2
    Local end_Point:TVector2

    'If vertically is true, then the line is horizontally, otherwise the line is vertically.
    If(horizontally) Then
        start_Point = TVector2.Create(position.get_X(), position.get_Y())
        end_Point = TVector2.Create(position.get_X() + render.get_Frame_Width() / 2.0, position.get_Y())
    Else
        start_Point = TVector2.Create(position.get_X(), position.get_Y())
        end_Point = TVector2.Create(position.get_X(), position.get_Y() + render.get_Frame_Height() / 2.0)
    EndIf

    'Calculate the normal by using two given points on the line.
    Return TVector2.make_Normal(start_Point, end_Point)
End Method

Rem
    bbdoc: This method does nothing in this system.
endrem
Method draw()

End Method
End Type

This is the vector class which is used in the system, just if someone needs to look up some methods:

Rem
    bbdoc: 2D Vector representation.
endrem
Type TVector2
    Field x:Float
    Field y:Float

    Rem
        bbdoc: Constructor. Creates vector which has the given components.
    endrem
    Function Create:TVector2(x:Float, y:Float)
        Local vec:TVector2 = New TVector2

        vec.x = x
        vec.y = y

        Return vec
    End Function

    '#region Getter/Setter
    Method get_X:Float()
        Return Self.x
    End Method

    Method get_Y:Float()
        Return Self.y
    End Method

    Method set_X(new_X:Float)
        Self.x = new_X
    End Method

    Method set_Y(new_Y:Float)
        Self.y = new_Y
    End Method

    Method get_Length:Float()
        Local x_Quad:Float = Self.x * Self.x
        Local y_Quad:Float = Self.y * Self.y

        Return Sqr(x_Quad + y_Quad)
    End Method
    '#endregion

    Method add_X(value:Float)
        Self.x = Self.x + value
    End Method

    Method add_Y(value:Float)
        Self.y = Self.y + value
    End Method

    Method add(to_Add:TVector2)
        Self.x = Self.x + to_add.get_X()
        Self.y = Self.y + to_add.get_Y()
    End Method

    Method scale(factor:Float)
        Self.x = Self.x * factor
        Self.y = Self.y * factor
    End Method

    Method scale_X(factor:Float)
        Self.x = Self.x * factor
    End Method

    Method scale_Y(factor:Float)
        Self.y = Self.y * factor
    End Method

    Method normalize()
        Local length:Float = Self.get_Length()
        Self.x = Self.x * (1.0 / length)
        Self.y = Self.y * (1.0 / length)
    End Method

    Method normalize_New:TVector2()
        Local length:Float = Self.get_Length()
        Local new_Vec:TVector2 = TVector2.Create(Self.x * (1.0 / length), Self.y * (1.0 / length))

        Return new_Vec
    End Method

    Method scalar_Multiplication:Float(other_Vector:TVector2)
        Return(Self.x * other_Vector.get_X() + Self.y * other_Vector.get_Y())
    End Method

    Method scale_New:TVector2(factor:Float)
        Local new_Vec:TVector2 = TVector2.Create(Self.x * factor, Self.y * factor)

        Return new_Vec
    End Method

    Method add_New:TVector2(to_Add:TVector2)
        Local new_Vec:TVector2 = TVector2.Create(Self.x + to_Add.get_X(), Self.y + to_Add.get_Y())

        Return new_Vec
    End Method


    Function make_Normal:TVector2(start_Position:TVector2, goal_Position:TVector2)
        Local directional_Vector:TVector2 = TVector2.Create(goal_Position.get_X() - start_Position.get_X(), goal_Position.get_Y() - start_Position.get_Y())
        Local normal_Vector:TVector2 = TVector2.Create(directional_Vector.get_Y(), -directional_Vector.get_X())
        normal_Vector.normalize()

        Return normal_Vector
    End Function
End Type

I tried to not tell in the comments what is done, but why it is done. This is pretty hard to do.

My actual question is now: What do you think about the code? How would you improve it? What is not so good about the code (e.g. performance, readability, maybe possible errors etc.)?

As far as I tried it, the code works for each possible case (ball hits boundaries, ball hits paddle from the side, ball hits paddle from up or down).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.