Pilot a Submarine- The Submarine
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 uptomaxWaterLevel
. Sets the flagwaterFilling
to true if the Ballast is to be filled with water, and the timerfillBallastTanks
is set tostart()
, which will increase the water level in the tank after every 500 millisecond.flushBallastTanks
: Flushes the Ballast tanks down to 0. Sets the flagwaterFlushing
to true if the Ballast is to be flushed out of water, and the timerflushBallastTanks
is set tostart()
, 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 */
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.
Let me know what you think of this article on twitter @RudraNilBasu or leave a comment below!