Thumbnail: kde

Pilot a Submarine- The Submarine

on under kde
16 minute read

In this post, I am going to discuss about the working of a submarine and my thought process on implementing the three basic features of a submarine in the “Pilot a Submarine” activity for the Qt version of GCompris, which are:

  • The Engine
  • The Ballast tanks and
  • The Diving Planes

The Engine

The engine of most submarines are either nuclear powered or diesel-electric engines, which are used to drive an electric motor which in turn, powers the submarine propellers. In this implementation, we will have two buttons one for increasing and another for decreasing the power generated by the submarine.

Ballast Tanks

The Ballast Tanks are the spaces in the submarine that can either be filled with water or air. It helps the submarine to dive and resurface on the water, using the concept of buouyancy. If the tanks are filled with water, the submarine dives underwater and if they are filled with air, it resurfaces on the surface of the water

Diving Planes

Once underwater, the diving planes of a submarine helps to accurately control the depth of the submarine. These are very similar to the fins present in the bodies of sharks, which helps them to swim and dive. When the planes are pointed downwards, the water flowing above the planes generate more pressure on the top surface than that on the bottom surface, forcing the submarine to dive deeper. This allows the driver to control the depth and the angle of the submarine.

Implementation

In this section I will be going through how I implemented the submarine using QML. For handling physics, I used Box2D.

The Submarine

The submarine is an QML Item element, designed as follows:

Item {
    id: submarine

    z: 1

    property point initialPosition: Qt.point(0,0)
    property bool isHit: false
    property int terminalVelocityIndex: 100
    property int resetVerticalSpeed: 500

    /* Maximum depth the submarine can dive when ballast tank is full */
    property real maximumDepthOnFullTanks: (background.height * 0.6) / 2

    /* Engine properties */
    property point velocity
    property int maximumXVelocity: 5

    /* Wings property */
    property int wingsAngle
    property int initialWingsAngle: 0
    property int maxWingsAngle: 2
    property int minWingsAngle: -2

    function destroySubmarine() {
        isHit = true
        submarineImage.broken()
    }

    function resetSubmarine() {
        isHit = false
        submarineImage.reset()

        leftBallastTank.resetBallastTanks()
        rightBallastTank.resetBallastTanks()
        centralBallastTank.resetBallastTanks()

        x = initialPosition.x
        y = initialPosition.y

        velocity = Qt.point(0,0)
        wingsAngle = initialWingsAngle
    }

	function increaseHorizontalVelocity(amt) {
        if (submarine.velocity.x + amt <= submarine.maximumXVelocity) {
            submarine.velocity.x += amt
        }
    }

    function decreaseHorizontalVelocity(amt) {
        if (submarine.velocity.x - amt >= 0) {
            submarine.velocity.x -= amt
        }
    }

    function increaseWingsAngle(amt) {
        if (wingsAngle + amt <= maxWingsAngle) {
            wingsAngle += amt
        } else {
            wingsAngle = maxWingsAngle
        }
    }

    function decreaseWingsAngle(amt) {
        if (wingsAngle - amt >= minWingsAngle) {
            wingsAngle -= amt
        } else {
            wingsAngle = minWingsAngle
        }
    }

    function changeVerticalVelocity() {
        /*
         * Movement due to planes
         * Movement is affected only when the submarine is moving forward
         * When the submarine is on the surface, the planes cannot be used
         */
        if (submarineImage.y > 0) {
            submarine.velocity.y = (submarine.velocity.x) > 0 ? wingsAngle : 0
        } else {
            submarine.velocity.y = 0
        }
        /* Movement due to Ballast tanks */
        if (wingsAngle == 0 || submarine.velocity.x == 0) {
            var yPosition = submarineImage.currentWaterLevel / submarineImage.totalWaterLevel * submarine.maximumDepthOnFullTanks

            speed.duration = submarine.terminalVelocityIndex * Math.abs(submarineImage.y - yPosition) // terminal velocity
            submarineImage.y = yPosition
        }
    }

    BallastTank {
        id: leftBallastTank

        initialWaterLevel: 0
        maxWaterLevel: 500
    }

    BallastTank {
        id: rightBallastTank

        initialWaterLevel: 0
        maxWaterLevel: 500
    }

    BallastTank {
        id: centralBallastTank

        initialWaterLevel: 0
        maxWaterLevel: 500
    }

    Image {
        id: submarineImage
        source: url + "submarine.png"

        property int currentWaterLevel: bar.level < 7 ? centralBallastTank.waterLevel : leftBallastTank.waterLevel + centralBallastTank.waterLevel + rightBallastTank.waterLevel
        property int totalWaterLevel: bar.level < 7 ? centralBallastTank.maxWaterLevel : leftBallastTank.maxWaterLevel + centralBallastTank.maxWaterLevel + rightBallastTank.maxWaterLevel

        width: background.width / 9
        height: background.height / 9

        function broken() {
            source = url + "submarine-broken.png"
        }

        function reset() {
            source = url + "submarine.png"
            speed.duration = submarine.resetVerticalSpeed
            x = submarine.initialPosition.x
            y = submarine.initialPosition.y
        }

        Behavior on y {
            NumberAnimation {
                id: speed
                duration: 500
            }
        }

        onXChanged: {
            if (submarineImage.x >= background.width) {
                Activity.finishLevel(true)
            }
        }
    }

    Body {
        id: submarineBody
        target: submarineImage
        bodyType: Body.Dynamic
        fixedRotation: true
        linearDamping: 0
        linearVelocity: submarine.isHit ? Qt.point(0,0) : submarine.velocity

        fixtures: Box {
            id: submarineFixer
            width: submarineImage.width
            height: submarineImage.height
            categories: items.submarineCategory
            collidesWith: Fixture.All
            density: 1
            friction: 0
            restitution: 0
            onBeginContact: {
                var collidedObject = other.getBody().target

                if (collidedObject == whale) {
                    whale.hit()
                }
                if (collidedObject == crown) {
                    crown.captureCrown()
                } else {
                    Activity.finishLevel(false)
                }
            }
        }
    }
    Timer {
        id: updateVerticalVelocity
        interval: 50
        running: true
        repeat: true

        onTriggered: submarine.changeVerticalVelocity()
    }
}

The Item is a parent object to hold all the different components of the submarine (the Image BallastTank and the Box2D component). It also contains the functions and the variables that are global to the submarine.

The Engine

The engine is a very straightforward implementation via the linearVelocity component of the Box2D element. We have two variables global to the submarine for handling the engine component, defined as follows:

property point velocity
property int maximumXVelocity: 5

which are pretty much self-explanatory, the velocity holds the current velocity of the submarine, both horizontal and vertical and the maximumXVelocity holds the maximum horizontal speed the submarine can achieve.

For increasing or decreasing the velocity of the submarine, we have two functions global to the submarine, as follows:

function increaseHorizontalVelocity(amt) {
    if (submarine.velocity.x + amt <= submarine.maximumXVelocity) {
        submarine.velocity.x += amt
    }
}

function decreaseHorizontalVelocity(amt) {
    if (submarine.velocity.x - amt >= 0) {
        submarine.velocity.x -= amt
    }
}

which essentially gets the amount by which the velocity.x component needs to be increased or decreased, checks whether it crosses the range or not, and makes the necessary changes likewise.

The actual applying of the velocity is very straightforward, which takes place in the Body component of the submarine as follows:

Body {
	...
	linearVelocity: submarine.isHit ? Qt.point(0,0) : submarine.velocity
	...

The submarine.isHit component, as the name suggests holds whether the submarine is hit by any object or not (except the pickups). If so, the velocity is reset to (0,0)

Thus, for increasing or decreasing the engine power, we just have to call one of the two functions anywhere from the code:

submarine.increaseHorizontalVelocity(1); /* For increasing H velocity */
submarine.decreaseHorizontalVelocity(1); /* For decreasing H velocity */

The Ballast Tanks

The Ballast Tanks are implemented separately in BallastTank.qml, since it will be implemented more that once. It looks like the following:

Item {
    property int initialWaterLevel
    property int waterLevel: 0
    property int maxWaterLevel
    property int waterRate: 10
    property bool waterFilling: false
    property bool waterFlushing: false

    function fillBallastTanks() {
        waterFilling = !waterFilling

        if (waterFilling) {
            fillBallastTanks.start()
        } else {
            fillBallastTanks.stop()
        }
    }

    function flushBallastTanks() {
        waterFlushing = !waterFlushing

        if (waterFlushing) {
            flushBallastTanks.start()
        } else {
            flushBallastTanks.stop()
        }
    }

    function updateWaterLevel(isInflow) {
        if (isInflow) {
            if (waterLevel < maxWaterLevel) {
                waterLevel += waterRate

            }
        } else {
            if (waterLevel > 0) {
                waterLevel -= waterRate
            }
        }

        if (waterLevel > maxWaterLevel) {
            waterLevel = maxWaterLevel
        }

        if (waterLevel < 0) {
            waterLevel = 0
        }
    }

    function resetBallastTanks() {
        waterFilling = false
        waterFlushing = false

        waterLevel = initialWaterLevel

        fillBallastTanks.stop()
        flushBallastTanks.stop()
    }

    Timer {
        id: fillBallastTanks
        interval: 500
        running: false
        repeat: true

        onTriggered: updateWaterLevel(true)
    }

    Timer {
        id: flushBallastTanks
        interval: 500
        running: false
        repeat: true

        onTriggered: updateWaterLevel(false)
    }
}

What they essentially does is:

  • fillBallastTanks: Fills up the Ballast tanks upto maxWaterLevel. Sets the flag waterFilling to true if the Ballast is to be filled with water, and the timer fillBallastTanks is set to start(), which will increase the water level in the tank after every 500 millisecond.
  • flushBallastTanks: Flushes the Ballast tanks down to 0. Sets the flag waterFlushing to true if the Ballast is to be flushed out of water, and the timer flushBallastTanks is set to start(), which will decrease the water level in the tank after every 500 millisecond.
  • resetBallastTanks: Resets the water level in the ballast tanks to it’s initial values

In the Submarine Item, we just use three instances of the BallastTank object, for left, right and central ballast tanks, setting up it’s initial and maximum water level.

BallastTank {
    id: leftBallastTank

    initialWaterLevel: 0
    maxWaterLevel: 500
}

BallastTank {
    id: rightBallastTank

    initialWaterLevel: 0
    maxWaterLevel: 500
}

BallastTank {
    id: centralBallastTank

    initialWaterLevel: 0
    maxWaterLevel: 500
}

For filling up or flushing the ballast tanks (centralBallastTank in this case), we just have two call either of the following two functions:

centralBallastTank.fillBallastTanks() /* For filling */
centralBallastTank.flushBallastTanks() /* For flushing */

I will be discussing about how the depth is maintained using the ballast tanks in the next section.

The Diving Planes

The diving planes will be used to control the depth of the submarine once it is moving underwater. Keeping that in mind, along with the fact that it needs to be effectively integrated with the ballast tanks. This is implemented in the changeVerticalVelocity() function, which is discussed as follows:

/*
 * Movement due to planes
 * Movement is affected only when the submarine is moving forward
 * When the submarine is on the surface, the planes cannot be used
 */
if (submarineImage.y > 0) {
    submarine.velocity.y = (submarine.velocity.x) > 0 ? wingsAngle : 0
} else {
    submarine.velocity.y = 0
}

However, under the following conditions:

  • the angle of the planes is reduced to 0
  • the horizontal velocity of the submarine is 0,

the ballast tanks will take over. Which is implemented as:

/* Movement due to Ballast tanks */
if (wingsAngle == 0 || submarine.velocity.x == 0) {
    var yPosition = submarineImage.currentWaterLevel / submarineImage.totalWaterLevel * submarine.maximumDepthOnFullTanks

    speed.duration = submarine.terminalVelocityIndex * Math.abs(submarineImage.y - yPosition) // terminal velocity
    submarineImage.y = yPosition
}

yPosition calculates how much percentage of the tank is filled with water, and likewise it determines the depth to which it will dive. The speed.duration is the duration of the transition animation, and the duration depends directly on how much the submarine will have to cover up along the Y axis, to avoid a steep rise or fall of the submarine.

For increasing or decreasing the angle of the diving planes, we just need to call either of the following two functions:

submarine.increaseWinglsAngle(1) /* For increasing */
submarine.decreaseWingsAngle(1) /* For decerasing */

img


That’s it for now! The two major goals to be completed next are the rotation of the submarine (in case more than one tanks are used and they are unequally filled up) and the UI for controlling the submarine. Will provide an update on it once it is completed.

kde, gcompris
comments powered by Disqus