Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/virtual joystick #63

Merged
merged 5 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"phaser": "^3.55.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-joystick-component": "^6.0.0",
"react-redux": "^7.2.5",
"sass": "^1.42.1",
"styled-components": "^5.3.5",
Expand Down
2 changes: 2 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import WhiteboardDialog from './components/WhiteboardDialog'
import VideoConnectionDialog from './components/VideoConnectionDialog'
import Chat from './components/Chat'
import HelperButtonGroup from './components/HelperButtonGroup'
import MobileVirtualJoystick from './components/MobileVirtualJoystick'

const Backdrop = styled.div`
position: absolute;
Expand Down Expand Up @@ -39,6 +40,7 @@ function App() {
<Chat />
{/* Render VideoConnectionDialog if user is not connected to a webcam. */}
{!videoConnected && <VideoConnectionDialog />}
<MobileVirtualJoystick />
</>
)
}
Expand Down
27 changes: 23 additions & 4 deletions client/src/characters/MyPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import store from '../stores'
import { pushPlayerJoinedMessage } from '../stores/ChatStore'
import { ItemType } from '../../../types/Items'
import { NavKeys } from '../../../types/KeyboardState'
import { JoystickMovement } from '../components/Joystick'

export default class MyPlayer extends Player {
private playContainerBody: Phaser.Physics.Arcade.Body
private chairOnSit?: Chair
public joystickMovement?: JoystickMovement
constructor(
scene: Phaser.Scene,
x: number,
Expand All @@ -41,6 +43,10 @@ export default class MyPlayer extends Player {
phaserEvents.emit(Event.MY_PLAYER_TEXTURE_CHANGE, this.x, this.y, this.anims.currentAnim.key)
}

handleJoystickMovement(movement: JoystickMovement) {
this.joystickMovement = movement
}

update(
playerSelector: PlayerSelector,
cursors: NavKeys,
Expand Down Expand Up @@ -121,13 +127,26 @@ export default class MyPlayer extends Player {
const speed = 200
let vx = 0
let vy = 0
if (cursors.left?.isDown || cursors.A?.isDown) vx -= speed
if (cursors.right?.isDown || cursors.D?.isDown) vx += speed
if (cursors.up?.isDown || cursors.W?.isDown) {

let joystickLeft = false
let joystickRight = false
let joystickUp = false
let joystickDown = false

if (this.joystickMovement?.isMoving) {
joystickLeft = this.joystickMovement.direction.left
joystickRight = this.joystickMovement.direction.right
joystickUp = this.joystickMovement.direction.up
joystickDown = this.joystickMovement.direction.down
}

if (cursors.left?.isDown || cursors.A?.isDown || joystickLeft) vx -= speed
if (cursors.right?.isDown || cursors.D?.isDown || joystickRight) vx += speed
if (cursors.up?.isDown || cursors.W?.isDown || joystickUp) {
vy -= speed
this.setDepth(this.y) //change player.depth if player.y changes
}
if (cursors.down?.isDown || cursors.S?.isDown) {
if (cursors.down?.isDown || cursors.S?.isDown || joystickDown) {
vy += speed
this.setDepth(this.y) //change player.depth if player.y changes
}
Expand Down
18 changes: 14 additions & 4 deletions client/src/characters/PlayerSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,23 @@ export default class PlayerSelector extends Phaser.GameObjects.Zone {

// update player selection box position so that it's always in front of the player
const { x, y } = player
if (cursors.left?.isDown || cursors.A?.isDown) {
let joystickLeft = false
let joystickRight = false
let joystickUp = false
let joystickDown = false
if (player.joystickMovement?.isMoving) {
joystickLeft = player.joystickMovement?.direction.left
joystickRight = player.joystickMovement?.direction.right
joystickUp = player.joystickMovement?.direction.up
joystickDown = player.joystickMovement?.direction.down
}
if (cursors.left?.isDown || cursors.A?.isDown || joystickLeft) {
this.setPosition(x - 32, y)
} else if (cursors.right?.isDown || cursors.D?.isDown) {
} else if (cursors.right?.isDown || cursors.D?.isDown || joystickRight) {
this.setPosition(x + 32, y)
} else if (cursors.up?.isDown || cursors.W?.isDown) {
} else if (cursors.up?.isDown || cursors.W?.isDown || joystickUp) {
this.setPosition(x, y - 32)
} else if (cursors.down?.isDown || cursors.S?.isDown) {
} else if (cursors.down?.isDown || cursors.S?.isDown || joystickDown) {
this.setPosition(x, y + 32)
}

Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import { MessageType, setFocused, setShowChat } from '../stores/ChatStore'

const Backdrop = styled.div`
position: fixed;
bottom: 0;
bottom: 60px;
left: 0;
height: 400px;
width: 500px;
max-height: 50%;
max-width: 50%;
max-width: 100%;
`

const Wrapper = styled.div`
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/ComputerDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const Wrapper = styled.div`

.close {
position: absolute;
top: 16px;
right: 16px;
top: 0px;
right: 0px;
}
`

Expand Down
12 changes: 11 additions & 1 deletion client/src/components/HelperButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import LightbulbIcon from '@mui/icons-material/Lightbulb'
import ArrowRightIcon from '@mui/icons-material/ArrowRight'
import GitHubIcon from '@mui/icons-material/GitHub'
import TwitterIcon from '@mui/icons-material/Twitter'
import VideogameAssetIcon from '@mui/icons-material/VideogameAsset'
import VideogameAssetOffIcon from '@mui/icons-material/VideogameAssetOff'

import { BackgroundMode } from '../../../types/BackgroundMode'
import { toggleBackgroundMode } from '../stores/UserStore'
import { setShowJoystick, toggleBackgroundMode } from '../stores/UserStore'
import { useAppSelector, useAppDispatch } from '../hooks'
import { getAvatarString, getColorByString } from '../util'

Expand Down Expand Up @@ -106,6 +108,7 @@ const StyledFab = styled(Fab)<{ target?: string }>`
export default function HelperButtonGroup() {
const [showControlGuide, setShowControlGuide] = useState(false)
const [showRoomInfo, setShowRoomInfo] = useState(false)
const showJoystick = useAppSelector((state) => state.user.showJoystick)
const backgroundMode = useAppSelector((state) => state.user.backgroundMode)
const roomJoined = useAppSelector((state) => state.room.roomJoined)
const roomId = useAppSelector((state) => state.room.roomId)
Expand All @@ -116,6 +119,13 @@ export default function HelperButtonGroup() {
return (
<Backdrop>
<div className="wrapper-group">
{roomJoined && (
<Tooltip title={showJoystick ? 'Disable virtual joystick' : 'Enable virtual joystick'}>
<StyledFab size="small" onClick={() => dispatch(setShowJoystick(!showJoystick))}>
{showJoystick ? <VideogameAssetOffIcon /> : <VideogameAssetIcon />}
</StyledFab>
</Tooltip>
)}
{showRoomInfo && (
<Wrapper>
<IconButton className="close" onClick={() => setShowRoomInfo(false)} size="small">
Expand Down
92 changes: 92 additions & 0 deletions client/src/components/Joystick.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Joystick } from 'react-joystick-component'

export interface JoystickMovement {
isMoving: boolean
direction: Direction
}

type Direction = {
jga-dev marked this conversation as resolved.
Show resolved Hide resolved
left: boolean
right: boolean
up: boolean
down: boolean
}

type Props = {
jga-dev marked this conversation as resolved.
Show resolved Hide resolved
onDirectionChange: (arg: JoystickMovement) => void
}

const angleToDirections = (angle: number, out?: Partial<Direction> | boolean): Direction => {
const outObj: Direction = {
left: false,
right: false,
up: false,
down: false,
...(typeof out === 'object' ? out : {}),
jga-dev marked this conversation as resolved.
Show resolved Hide resolved
}

outObj.left = false
outObj.right = false
outObj.up = false
outObj.down = false

angle = (angle + 360) % 360

if (angle > 22.5 && angle <= 67.5) {
outObj.down = true
outObj.right = true
} else if (angle > 67.5 && angle <= 112.5) {
outObj.down = true
} else if (angle > 112.5 && angle <= 157.5) {
outObj.down = true
outObj.left = true
} else if (angle > 157.5 && angle <= 202.5) {
outObj.left = true
} else if (angle > 202.5 && angle <= 247.5) {
outObj.left = true
outObj.up = true
} else if (angle > 247.5 && angle <= 292.5) {
outObj.up = true
} else if (angle > 292.5 && angle <= 337.5) {
outObj.up = true
outObj.right = true
} else {
outObj.right = true
}
return outObj
}

const JoystickItem = (props: Props) => {
return (
<Joystick
size={75}
baseColor="#4b4b4b70"
stickColor="#42eacb80"
stop={() => {
props.onDirectionChange({
isMoving: false,
direction: {
left: false,
right: false,
up: false,
down: false,
},
})
}}
move={(event) => {
const x1 = 0
const y1 = event.y ?? 0
const x2 = event.x ?? 0
const y2 = 0
var deltaX = x2 - x1 // distance between joystick and center
var deltaY = y2 - y1 // distance between joystick and center
var rad = Math.atan2(deltaY, deltaX) // In radians
var deg = (rad * 180) / Math.PI // In degrees
var direction = angleToDirections(deg, true) // Convert degrees to direction
props.onDirectionChange({ isMoving: true, direction })
}}
/>
)
}

export default JoystickItem
73 changes: 73 additions & 0 deletions client/src/components/MobileVirtualJoystick.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useEffect, useLayoutEffect, useState } from 'react'
import styled from 'styled-components'
import JoystickItem from './Joystick'

import phaserGame from '../PhaserGame'
import Game from '../scenes/Game'

import { useAppSelector } from '../hooks'
import { JoystickMovement } from './Joystick'

const Backdrop = styled.div`
position: fixed;
bottom: 100px;
right: 32px;
max-height: 50%;
max-width: 100%;
`

const Wrapper = styled.div`
position: relative;
height: 100%;
padding: 16px;
display: flex;
flex-direction: column;
`

const JoystickWrapper = styled.div`
margin-top: auto;
align-self: flex-end;
`
export const smallScreenSize = 650 // minimum width for small screen

function useWindowSize() {
const [size, setSize] = useState([0, 0])
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight])
}
window.addEventListener('resize', updateSize)
updateSize()
return () => window.removeEventListener('resize', updateSize)
}, [])
return size
}

const isSmallScreen = () => {
const [width] = useWindowSize()
return width <= smallScreenSize
}

export default function MobileVirtualJoystick() {
const showJoystick = useAppSelector((state) => state.user.showJoystick)
const showChat = useAppSelector((state) => state.chat.showChat)
const game = phaserGame.scene.keys.game as Game

useEffect(() => {}, [showJoystick, showChat])

const handleMovement = (movement: JoystickMovement) => {
game.myPlayer?.handleJoystickMovement(movement)
}

return (
<Backdrop>
<Wrapper>
{!(showChat && isSmallScreen) && showJoystick && (
jga-dev marked this conversation as resolved.
Show resolved Hide resolved
<JoystickWrapper>
<JoystickItem onDirectionChange={handleMovement}></JoystickItem>
</JoystickWrapper>
)}
</Wrapper>
</Backdrop>
)
}
9 changes: 6 additions & 3 deletions client/src/components/WhiteboardDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const Backdrop = styled.div`
height: 100vh;
overflow: hidden;
padding: 16px 180px 16px 16px;
width: 100%;
height: 100%;
`
const Wrapper = styled.div`
width: 100%;
Expand All @@ -25,19 +27,20 @@ const Wrapper = styled.div`
position: relative;
display: flex;
flex-direction: column;
min-width: max-content;

.close {
position: absolute;
top: 16px;
right: 16px;
top: 0px;
right: 0px;
}
`

const WhiteboardWrapper = styled.div`
flex: 1;
border-radius: 25px;
overflow: hidden;
margin-right: 50px;
margin-right: 25px;

iframe {
width: 100%;
Expand Down
5 changes: 5 additions & 0 deletions client/src/stores/UserStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const userSlice = createSlice({
videoConnected: false,
loggedIn: false,
playerNameMap: new Map<string, string>(),
showJoystick: window.innerWidth < 650,
},
reducers: {
toggleBackgroundMode: (state) => {
Expand All @@ -43,6 +44,9 @@ export const userSlice = createSlice({
removePlayerNameMap: (state, action: PayloadAction<string>) => {
state.playerNameMap.delete(sanitizeId(action.payload))
},
setShowJoystick: (state, action: PayloadAction<boolean>) => {
state.showJoystick = action.payload
},
},
})

Expand All @@ -53,6 +57,7 @@ export const {
setLoggedIn,
setPlayerNameMap,
removePlayerNameMap,
setShowJoystick,
} = userSlice.actions

export default userSlice.reducer
Loading