Java tutorial
/* * Copyright 2017 Aurlien Gteau <mail@agateau.com> * * This file is part of Pixel Wheels. * * Tiny Wheels is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package com.agateau.pixelwheels.racer; import com.agateau.pixelwheels.Constants; import com.agateau.pixelwheels.GamePlay; import com.agateau.pixelwheels.GameWorld; import com.agateau.pixelwheels.utils.Box2DUtils; import com.agateau.utils.AgcMathUtils; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Shape2D; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Body; import com.badlogic.gdx.physics.box2d.BodyDef; import com.badlogic.gdx.physics.box2d.FixtureDef; import com.badlogic.gdx.physics.box2d.joints.RevoluteJoint; import com.badlogic.gdx.physics.box2d.joints.RevoluteJointDef; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.ArrayMap; import com.badlogic.gdx.utils.Disposable; /** * Represents a car on the world */ public class Vehicle implements Racer.Component, Disposable { private static final float ACCELERATION_DELTA = 1; private static final float BRAKING_DELTA = 0.8f; public static class WheelInfo { public Wheel wheel; public RevoluteJoint joint; public float steeringFactor; } private final Body mBody; private final GameWorld mGameWorld; private Racer mRacer; private final TextureRegion mRegion; private final Array<WheelInfo> mWheels = new Array<WheelInfo>(); private String mId; private String mName; private int mCollisionCategoryBits; private int mCollisionMaskBits; private boolean mAccelerating = false; private boolean mBraking = false; private float mZ = 0; private float mDirection = 0; private float mTurboTime = -1; private boolean mStopped = false; private float mSpeedLimiter = 1f; private Probe mProbe = null; private ArrayMap<Long, Float> mTurboCellMap = new ArrayMap<Long, Float>(8); public Vehicle(TextureRegion region, GameWorld gameWorld, float originX, float originY, Array<Shape2D> shapes, float angle) { mGameWorld = gameWorld; // Main mRegion = region; // Body BodyDef bodyDef = new BodyDef(); bodyDef.type = BodyDef.BodyType.DynamicBody; bodyDef.position.set(originX, originY); bodyDef.angle = angle * MathUtils.degreesToRadians; mBody = mGameWorld.getBox2DWorld().createBody(bodyDef); // Body fixtures for (Shape2D shape : shapes) { FixtureDef fixtureDef = new FixtureDef(); fixtureDef.shape = Box2DUtils.createBox2DShape(shape, Constants.UNIT_FOR_PIXEL); fixtureDef.density = GamePlay.instance.vehicleDensity / 10.0f; fixtureDef.friction = 0.2f; fixtureDef.restitution = GamePlay.instance.vehicleRestitution / 10.0f; mBody.createFixture(fixtureDef); fixtureDef.shape.dispose(); } } @Override public void dispose() { for (WheelInfo info : mWheels) { info.wheel.dispose(); } mGameWorld.getBox2DWorld().destroyBody(mBody); } public WheelInfo addWheel(TextureRegion region, float x, float y, float angle) { WheelInfo info = new WheelInfo(); info.wheel = new Wheel(mGameWorld, this, region, getX() + x, getY() + y, angle); mWheels.add(info); Body body = info.wheel.getBody(); body.setUserData(mBody.getUserData()); RevoluteJointDef jointDef = new RevoluteJointDef(); // Call initialize() instead of defining bodies and anchors manually. Defining anchors manually // causes Box2D to move the car a bit while it solves the constraints defined by the joints jointDef.initialize(mBody, body, body.getPosition()); jointDef.lowerAngle = 0; jointDef.upperAngle = 0; jointDef.enableLimit = true; info.joint = (RevoluteJoint) mGameWorld.getBox2DWorld().createJoint(jointDef); return info; } public void setRacer(Racer racer) { mRacer = racer; mBody.setUserData(racer); for (WheelInfo info : mWheels) { info.wheel.getBody().setUserData(racer); } } public void setProbe(Probe probe) { mProbe = probe; } public void setCollisionInfo(int categoryBits, int maskBits) { mCollisionCategoryBits = categoryBits; mCollisionMaskBits = maskBits; applyCollisionInfo(); } public Array<WheelInfo> getWheelInfos() { return mWheels; } public String getId() { return mId; } public void setId(String id) { mId = id; } public Body getBody() { return mBody; } public TextureRegion getRegion() { return mRegion; } public float getSpeed() { return mBody.getLinearVelocity().len(); } public boolean isDrifting() { for (WheelInfo wheelInfo : mWheels) { if (wheelInfo.wheel.isDrifting()) { return true; } } return false; } /** * speedLimiter is a percentage. Set it to 0.9 to make the vehicle drive at 90% of its maximum speed */ public void setSpeedLimiter(float speedLimiter) { mSpeedLimiter = speedLimiter; } /** * Returns the angle the car is facing */ public float getAngle() { return AgcMathUtils.normalizeAngle(mBody.getAngle() * MathUtils.radiansToDegrees); } public float getWidth() { return Constants.UNIT_FOR_PIXEL * mRegion.getRegionWidth(); } public float getHeight() { return Constants.UNIT_FOR_PIXEL * mRegion.getRegionHeight(); } public boolean isFlying() { return mZ > 0; } public boolean isFalling() { return mZ < 0; } public float getZ() { return mZ; } public void setZ(float z) { boolean wasFlying = mZ > 0; boolean flying = z > 0; if (!wasFlying && flying) { Box2DUtils.setCollisionInfo(mBody, 0, 0); for (WheelInfo info : mWheels) { Box2DUtils.setCollisionInfo(info.wheel.getBody(), 0, 0); } } else if (wasFlying && !flying) { applyCollisionInfo(); } mZ = z; } /** * Call this when the vehicle needs to stop as soon as possible * For example because it fell */ public void setStopped(boolean stopped) { if (stopped) { mTurboTime = -1; } mStopped = stopped; } @Override public void act(float dt) { if (!isFlying()) { if (mStopped) { actStopping(dt); } else { applyTurbo(dt); applyPilotCommands(); applyGroundEffects(dt); } } actWheels(dt); } private void actStopping(float dt) { Vector2 invVelocity = mBody.getLinearVelocity().scl(-0.1f); mBody.applyForce(invVelocity.scl(mBody.getMass()).scl(1 / dt), mBody.getWorldCenter(), true); } /** * Apply ground effects on the vehicle: * - trigger turbo when driving on turbo tiles * - apply drag */ private void applyGroundEffects(float dt) { final GamePlay GP = GamePlay.instance; float groundSpeed = 0; for (WheelInfo info : mWheels) { float wheelGroundSpeed = info.wheel.getGroundSpeed(); groundSpeed += wheelGroundSpeed; long cellId = info.wheel.getCellId(); boolean isTurboCell = wheelGroundSpeed > 1; if (isTurboCell && !alreadyTriggeredTurboCell(cellId)) { triggerTurbo(); addTriggeredTurboCell(cellId); } } groundSpeed /= mWheels.size; updateTriggeredTurboTiles(dt); boolean turboOn = mTurboTime > 0; if (groundSpeed < 1f && !turboOn) { Box2DUtils.applyDrag(mBody, (1 - groundSpeed) * GP.groundDragFactor); } } /** * Apply pilot commands to the wheels */ private void applyPilotCommands() { float speedDelta = 0; if (mGameWorld.getState() == GameWorld.State.RUNNING) { if (mAccelerating) { speedDelta = ACCELERATION_DELTA * mSpeedLimiter; } if (mBraking) { speedDelta -= BRAKING_DELTA; } } float steerAngle = computeSteerAngle() * MathUtils.degRad; for (WheelInfo info : mWheels) { float angle = info.steeringFactor * steerAngle; info.wheel.adjustSpeed(speedDelta); info.joint.setLimits(angle, angle); } } private void actWheels(float dt) { for (WheelInfo info : mWheels) { info.wheel.act(dt); } } private final Vector2 mDirectionVector = new Vector2(); private Vector2 computeDirectionVector(float strength) { return mDirectionVector.set(strength, 0).rotateRad(mBody.getAngle()); } private void applyTurbo(float dt) { final GamePlay GP = GamePlay.instance; if (mTurboTime == 0) { mBody.applyLinearImpulse(computeDirectionVector(GP.turboStrength / 6), mBody.getWorldCenter(), true); } if (mTurboTime >= 0) { mTurboTime += dt; mBody.applyForce(computeDirectionVector(GP.turboStrength), mBody.getWorldCenter(), true); if (mTurboTime > GP.turboDuration) { mTurboTime = -1; } } } private float computeSteerAngle() { final GamePlay GP = GamePlay.instance; if (mDirection == 0) { if (mProbe != null) { float speed = mBody.getLinearVelocity().len() * Box2DUtils.MS_TO_KMH; mProbe.addValue("steer", 0); mProbe.addValue("speed", speed); mProbe.addValue("category", 0); } return 0; } float speed = mBody.getLinearVelocity().len() * Box2DUtils.MS_TO_KMH; float steer; // Category is 0 if speed is < GP.lowSpeed, 1 if < GP.maxSpeed, 2 if > GP.maxSpeed // For a better driving experience, it should not reach 2 except when triggering turbos float category; if (speed < GP.lowSpeed) { steer = MathUtils.lerp(GP.stoppedMaxSteer, GP.lowSpeedMaxSteer, speed / GP.lowSpeed); category = 0; } else if (speed < GP.maxSpeed) { float factor = (speed - GP.lowSpeed) / (GP.maxSpeed - GP.lowSpeed); steer = MathUtils.lerp(GP.lowSpeedMaxSteer, GP.highSpeedMaxSteer, factor); category = 1; } else { steer = GP.highSpeedMaxSteer; category = 2; } if (mProbe != null) { mProbe.addValue("steer", steer); mProbe.addValue("speed", speed); mProbe.addValue("category", category); } return mDirection * steer; } private boolean alreadyTriggeredTurboCell(long cellId) { return mTurboCellMap.containsKey(cellId); } private void addTriggeredTurboCell(long cellId) { mTurboCellMap.put(cellId, GamePlay.instance.turboDuration); } private void updateTriggeredTurboTiles(float delta) { for (int idx = mTurboCellMap.size - 1; idx >= 0; --idx) { float duration = mTurboCellMap.getValueAt(idx) - delta; if (duration <= 0) { mTurboCellMap.removeIndex(idx); } else { mTurboCellMap.setValue(idx, duration); } } } public void setAccelerating(boolean value) { mAccelerating = value; } public void setBraking(boolean value) { mBraking = value; } public boolean isBraking() { return mBraking; } public void setDirection(float direction) { mDirection = direction; } public Vector2 getPosition() { return mBody.getPosition(); } public float getX() { return mBody.getPosition().x; } public float getY() { return mBody.getPosition().y; } public void setName(String name) { mName = name; } public String getName() { return mName; } public float getTurboTime() { return mTurboTime; } public void triggerTurbo() { mRacer.getAudioComponent().triggerTurbo(); mTurboTime = 0; } private void applyCollisionInfo() { Box2DUtils.setCollisionInfo(mBody, mCollisionCategoryBits, mCollisionMaskBits); for (WheelInfo info : mWheels) { Box2DUtils.setCollisionInfo(info.wheel.getBody(), mCollisionCategoryBits, mCollisionMaskBits); } } @Override public String toString() { return getName(); } }