Make a simple first person smartwatch

Hey there!
I want to add a smartwatch mechanic to my first person game. It should look like this:

Not equipped:

Equipped:

So basically when the right mouse button is being pressed the arm “fades” into the screen stopping the player from rotating their head and only allowing them to rotate the camera a tiny bit to control the touch screen of the smartwatch. Once unequipped, the player can move and rotate normally. Can anyone help me with this?

2 Likes

I highly recommend looking into ViewModels. ViewModels are essentially a clone of your character, except it’s controlled entirely independently from your movement. I’ve written up a sample code for how you’d set up a basic ViewModel, note this is to support your understanding of it, I still strongly urge you to look into ViewModels. They are used in FPS games, but I see a potential for them to be used in this context as well.
Firstly, you need to setup the ViewModel by cloning the character - then you need to setup the ViewModel’s HRP, in this case, it’s called “PsuedoHRP”, it’ll act as a camera later on. We then loop through the bodyparts of the ViewModel to destroy any unnecessary items, we only want the limbs at this moment.
Note, this is all in one local script located in StarterCharacterScripts.

local viewModel,psuedoHRP,watch;

local IN_VIEW = false

local LoadedAnimations = {} --// STORE CHARACTER ANIMATIONS LOADED INTO THE VIEWMODEL

--// FUNCTIONS
function GenerateViewmodel()
	char.Archivable = true -- ALLOW CLONING OF THE CHARACTER
	viewModel = char:Clone()
	viewModel.Parent = camera
	viewModel.Name = "ViewModel"
	char.Archivable = false

	psuedoHRP = Instance.new("Part") -- "PSEUDOHRP" ACTS AS THE VIEWMODEL CAMERA
	psuedoHRP.Name = "ViewmodelHRP"
	psuedoHRP.Size = Vector3.new(0.5,0.5,2)
	psuedoHRP.Anchored = true
	psuedoHRP.CanCollide = false
	psuedoHRP.Parent = camera
	psuedoHRP.Transparency = 1
		
	--// FOR THE SAKE OF SIMPLICITY, I'M ASSIGNING THE ATTACHMENT A POSITION WHICH SHOULD REPRESENT THE ACTUAL WATCH MODEL ON THE ARM
	watch = Instance.new("Attachment")
	watch.Parent = char.RightLowerArm
	watch.Position = Vector3.new(0, -0.107, -0.51)
	
	--// DELETE UNNECCESSARY BODY PARTS
	for _,bodyPart in pairs(viewModel:GetDescendants()) do
		if bodyPart:IsA("BasePart") then
			bodyPart.CastShadow = false
			bodyPart.CollisionGroup = "ViewModel"

			local LowerName = bodyPart.Name:lower()
			if LowerName:match("leg") or LowerName:match("foot") then
				bodyPart:Destroy()
			elseif not (LowerName:match("arm") or LowerName:match("hand")) then
				bodyPart.Transparency = 1
			end
		elseif bodyPart:IsA("Decal") then
			bodyPart:Destroy()
		elseif bodyPart:IsA("Accessory") then
			bodyPart:Destroy()
		elseif bodyPart:IsA("LocalScript") then
			bodyPart:Destroy()
		end
	end
end

Secondly, (the simplest part) you’ll need to link this all into a RenderStepped loop. We want to move the ViewModel and the ViewModel’s camera for each frame, as well as updating the animation for the ViewModel, which I’ll explain further along. It should look something like this.

RunService.RenderStepped:Connect(function()
	if not (viewModel or psuedoHRP) then return end --// IF EITHER AREN'T LOADED, DON'T PROGRESS FURTHER
	_UpdateAnimations()
	viewModel:SetPrimaryPartCFrame(camera.CFrame)
	if not IN_VIEW then
		psuedoHRP.CFrame = camera.CFrame --// AS THE VIEWMODEL HRP ACTS AS A CAMERA, IT'LL NEED TO REPLICATE THE CAMERA CFRAME FOR EACH FRAME.
	end
end)

Then you’ll need to either create an animation that extends the arm into the FOV of the player or manually reposition the arm using CFrames. At this point, your ViewModel should be able to replicate animations being played by the character. I’d personally recommend using an animation, it’s simpler and less messy, but to each their own. I’ve written a sample code for replicating the animations if you’re struggling with this part.
The animator in reference will be the characters animator, we want to always fire an event whenever the character is playing an animation. Moreover, you’ll want to assign an attribute to your loaded animations to filter out animations that may be unrelated to the ViewModel with those that are (focusing the watch for instance).
We’ll also need to update the animation whenever it’s updated on the Character to the Viewmodel. _UpdateAnimation() will therefore need to run in the RenderStepped loop.

function _UpdateAnimations()
	for CharAnim,Anim in pairs(LoadedAnimations) do
		if CharAnim.IsPlaying ~= Anim.IsPlaying then
			if CharAnim.IsPlaying then
				Anim:Play()
			else
				Anim:Stop()
			end
		end

		Anim.TimePosition = CharAnim.TimePosition
		Anim:AdjustWeight(CharAnim.WeightCurrent, 0)
	end
end

function GenerateViewmodel()
    --// THIS WILL BE IN ADDITION TO WHAT WAS WRITTEN ABOVE, THIS SHOULD RUN ALONGSIDE THE SETUP CALL FOR A VIEWMODEL TO INITIATE THE ANIMATOR.
	local viewModelAnimator = viewModel.Humanoid.Animator --// REPLICATE CHARACTER ANIMATIONS ONTO VIEWMODEL
	viewModel.Humanoid.BreakJointsOnDeath = false
	viewModel.Head.Anchored = true
	viewModel.PrimaryPart = viewModel.Head
	viewModel:SetPrimaryPartCFrame(CFrame.new(0, 0, 0))
end

local exampleAnim = animator:LoadAnimation(watch.Animation) --// LOAD EXAMPLE ANIMATION
exampleAnim:SetAttribute("WatchAnimation",true) --// ASSIGN EXAMPLE ANIMATION UNIQUE ATTRIBUTE TO IDENTIFY WHETHER TO PLAY ON VIEWMODEL

animator.AnimationPlayed:Connect(function(AnimationTrack)
		if AnimationTrack:GetAttribute("WatchAnimation") ~= true then return end --// IF ANIMATION ISN'T LINKED TO THE VIEWMODEL, SKIP OVER IT. (FILTER)

		if not LoadedAnimations[AnimationTrack] then --// INDEX THE LOADED ANIMATIONS USING THE ANIMATION TRACK
			LoadedAnimations[AnimationTrack] = viewModelAnimator:LoadAnimation(AnimationTrack.Animation) --// LOAD ANIMATION FOR VIEWMODEL ANIMATOR
		end
	end)

Finally, you’ll need to change your camera movement to focus on the “PsuedoHRP” part. This will lock the camera to a part, allowing for slight mouse/camera movement. This is what that should look like. The camera movement will follow the mouse, and should be similar to what you’ve described.

function AdjustCamera(toggle)
	local maxTilt = 10 --// CLAMP THE ANGLE IN WHICH THE CAMERA WILL TILT
	
	local function Active()
		camera.CFrame = psuedoHRP.CFrame * CFrame.Angles(
			math.rad((((mouse.Y - mouse.ViewSizeY / 2) / mouse.ViewSizeY)) * -maxTilt),
			math.rad((((mouse.X - mouse.ViewSizeX / 2) / mouse.ViewSizeX)) * -maxTilt),
			0
		)
	end
	
	if toggle then
		camera.CameraType = Enum.CameraType.Scriptable

		IN_VIEW = true
		psuedoHRP.CFrame = CFrame.lookAt(psuedoHRP.Position, watch.Position) --// POINT "CAMERA" TO THE WATCH

		RunService:BindToRenderStep("ActiveCamera",Enum.RenderPriority.Camera.Value,Active)
	else
		RunService:UnbindFromRenderStep("ActiveCamera")
		camera.CameraType = Enum.CameraType.Custom
		IN_VIEW = false
	end
end

I hope this is useful, please let me know if you have any questions regarding this, I rushed this out a little so there may be some mistakes. This code provided by all means will not be able to complete what you’re looking to accomplish, I hope they provide a strong basis on how to achieve it though. They shouldn’t require too much adjustment to solve, but again, if you need additional support I’m happy to answer any other questions. I don’t want to spoon feed what you’re trying to accomplish, but I’ve tried to explain each step as much as I could.
Also, the ability to focus in on the watch with a tool being equipped can simply be done with the tool.equipped event. Moreover, you could simply use UserInputService to initiate the AdjustCamera() function when either mouse button is clicked or held down.